我个人对陷阱的定义是这样的: 代码看起来可以工作, 但不是以你 "想当然"" 的方式. 如果一段代码直接出错, 抛出了异常, 我不认为这是陷阱. 比如, Python 程序员应该都遇到过的 "UnboundLocalError", 示例:
对于 "UnboundLocalError", 还有更高级的版本:
可能对于很多 python 新手来说, 这个 Error 让人摸不着头脑. 但我认为这不算陷阱, 因为这段代码一定会报错, 而不是默默的以错误的方式运行. 不怕真小人, 就怕伪君子. 我认为缺陷就好比伪君子.
那么 Python 中哪些真正算得上陷阱呢
第一: 以 mutable 对象作为默认参数
这个估计是最广为人知的了, Python 和其他很多语言一样, 提供了默认参数, 默认参数确实是个好东西, 可以让函数调用者忽略一些细节(比如 GUI 编程, Tkinter,QT), 对于 lambda 表达式也非常有用. 但是如果使用了可变对象作为默认参数, 那么事情就不那么愉快了
惊喜不惊喜?! 究其原因, python 中一切都是对象, 函数也不列外, 默认参数只是函数的一个属性. 而默认参数在函数定义的时候已经求值了.
Default parameter values are evaluated when the function definition is executed.
Stack Overflow 上有一个更适当的例子来说明默认参数是在定义的时候求值, 而不是调用的时候.
python docoment 给出了标准的解决办法:
A way around this is to use None as the default, and explicitly test for it in the body of the function
第二: x += y vs x = x + y
一般来说, 二者是等价的, 至少看起来是等价的(这也是陷阱的定义 - 看起来都 OK, 但不一定正确).
呃, 被光速打脸了?
前者 x 指向一个新的对象, 后者 x 在原来的对象是修改, 当然, 那种效果是正确的取决于应用场景. 至少, 得知道, 二者有时候并不一样
第三, 神奇的小括号 -()
小括号 (parenthese) 在各种编程语言中都有广泛的应用, python 中, 小括号还能表示元组 (tuple) 这一数据类型, 元组是 immutable 的序列.
但如果只有一个元素呢
神奇不神奇, 如果要表示只有一个元素的元组, 正确的姿势是:
第四: 生成一个元素是列表的列表
这个有点像二维数组, 当然生成一个元素是字典的列表也是可以的, 更通俗的说, 生成一个元素是可变对象的序列
很简单嘛:
看起来很不错, 简单明了, but
我猜, 这应该不是你预期的结果吧, 究其原因, 还是因为 python 中 list 是可变对象, 上述的写法大家都指向的同一个可变对象, 正确的姿势
另外一个在实际编码中遇到的问题, dict.fromkeys, 也有异曲同工之妙: 创建的 dict 的所有 values 指向同一个对象.
fromkeys(seq[, value])
Create a new dictionary with keys from seq and values set to value.
第五, 在访问列表的时候, 修改列表
列表 (list) 在 python 中使用非常广泛, 当然经常会在访问列表的时候增加或者删除一些元素. 比如, 下面这个函数, 试图删掉列表中为 3 的倍数的元素:
测试一下,
好像没什么错, 不过这只是运气好
上面的例子中, 6 这个元素就没有被删除. 如果在 modify_lst 函数中 print idx, item 就可以发现端倪: lst 在变短, 但 idx 是递增的, 所以在上面出错的例子中, 当 3 被删除之后, 6 变成了 lst 的第 2 个元素(从 0 开始). 在 C++ 中, 如果遍历容器的时候用迭代器删除元素, 也会有同样的问题.
如果逻辑比较简单, 使用 list comprehension 是不错的注意
第六, 闭包与 lambda
这个也是老生长谈的例子, 在其他语言也有类似的情况. 先看一个例子:
create_multipliers 函数的返回值时一个列表, 列表的每一个元素都是一个函数 -- 将输入参数 x 乘以一个倍数 i 的函数. 预期的结果时 0,2,4,6,8. 但结果是 5 个 8, 意外不意外.
由于出现这个陷阱的时候经常使用了 lambda, 所以可能会认为是 lambda 的问题, 但 lambda 表示不愿意背这个锅. 问题的本质在与 python 中的属性查找规则, LEGB(local,enclousing,global,bulitin), 在上面的例子中, i 就是在闭包作用域(enclousing), 而 Python 的闭包是 迟绑定 , 这意味着闭包中用到的变量的值, 是在内部函数被调用时查询得到的.
解决办法也很简单, 那就是变闭包作用域为局部作用域.
第七, 定义 del
大多数计算机专业的同学可能都是先学的 C,C++, 构造, 析构函数的概念应该都非常熟. 于是, 当切换到 python 的时候, 自然也想知道有没有相应的函数. 比如, 在 C++ 中非常有名的 RAII, 即通过构造, 析构来管理资源 (如内存, 文件描述符) 的声明周期. 那在 python 中要达到同样的效果怎么做呢, 即需要找到一个对象在销毁的时候一定会调用的函数, 于是发现了 init, del 函数, 可能简单写了两个例子发现确实也能工作. 但事实上可能掉进了一个陷阱, 在 python documnet 是有描述的:
Circular references which are garbage are detected when the option cycle detector is enabled (it's on by default), but can only be cleaned up if there are no Python-level del() methods involved.
简单来说, 如果在循环引用中的对象定义了 del, 那么 python gc 不能进行回收, 因此, 存在内存泄漏的风险
第八, 不同的姿势 import 同一个 module
示例在 Stack Overflow 的例子上稍作修改, 假设现在有一个 package 叫 mypackage, 里面包含三个 python 文件: mymodule.py, main.py, init.py.mymodule.py 代码如下:
main.py 代码如下:
运行 python main.py, 结果如下:
- updated list [1] 4406700752
- module in get 4406700920
- lets check []
从运行结果可以看到, 在 add 和 get 函数中 import 的 mymodule 不是同一个 module,ID 不同. 当然, 在 python2.7.10 中, 需要 main.py 的第 13 行才能出现这样的效果. 你可能会问, 谁会写出第 13 行这样的代码呢? 事实上, 在很多项目中, 为了 import 的时候方便, 会往 sys.path 加入一堆路径. 那么在项目中, 大家同意一种 import 方式就非常有必要了
第九, python 升级
python3.x 并不向后兼容, 所以如果从 2.x 升级到 3.x 的时候得小心了, 下面列举两点:
在 python2.7 中, range 的返回值是一个列表; 而在 python3.x 中, 返回的是一个 range 对象.
map(),filter(), dict.items()在 python2.7 返回列表, 而在 3.x 中返回迭代器. 当然迭代器大多数都是比较好的选择, 更加 pythonic, 但是也有缺点, 就是只能遍历一次. 在 instagram 的分享中, 也提到因为这个导致的一个坑爹的 bug.
第十:++i -i
这个陷阱主要是坑来自 C,C++ 背景的同学. 简单来说,++i 是对 i 取两次正号,-i 是对 i 取两次负号, 运算完之后 i 的值不变.
第十一: setattr getattr getattribute
Python 中有大量的 magic method(形似 xx), 其中许多跟属性访问有关, 比如 get, set,delete_,getattr,setattr,delattr,getattribute. 前三个跟 descriptor 相关, 详细可参见《python descriptor 详解》. 坑爹的是, getattr 与 setattr 相差很大, 在《python 属性查找(attribute look up)》一文中有详细介绍. 简单说来, setattr 与 getattribute 是对应的, 都是修改 python 默认的属性修改, 查找机制, 而 getattr 只是默认查找机制无法找到属性的时候才会调用, setattr 应该叫 setattribute__才恰当!
第负一, gil
以 GIL 结尾, 因为 gil 是 Python 中大家公认的缺陷!
其他语言过来的同学可能看到 python 用 threading 模块, 拿过来就用, 结果发现效果不对啊, 然后就会喷, 什么鬼
总结:
毫无疑问的说, python 是非常容易上手, 也非常强大的一门语言. python 非常灵活, 可定制化很强. 同时, 也存在一些陷阱, 搞清楚这些陷阱能够更好的掌握, 使用这么语言. 本文列举了一些 python 中的一些缺陷, 这是一份不完全列表, 欢迎大家补充.
来源: https://juejin.im/post/5c053e566fb9a049d37edb5c