关于 Python
Python 是一门解释性的,面向对象的,并具有动态语义的高级编程语言。它高级的内置数据结构,结合其动态类型和动态绑定的特性,使得它在快速应用程序开发( Rapid Application Development )中颇为受欢迎,同时 Python 还能作为脚本语言或者胶水语言讲现成的组件或者服务结合起来。 Python 支持模块( modules )和包( packages ),所以也鼓励程序的模块化以及代码重用。
关于本文
Python 简单、易学的语法可能会误导一些 Python 程序员(特别是那些刚接触这门语言的人们),可能会忽略某些细微之处和这门语言的强大之处。
考虑到这点,本文列出了“十大”甚至是高级的 Python 程序员都可能犯的,却又不容易发现的细微错误。(注意:本文是针对比《 Python 程序员常见错误》(
https://uqer.io/community/share/57218746228e5b633c7b93ee )稍微高级一点读者,对于更加新手一点的 Python 程序员,有兴趣可以读一读那篇文章)
常见错误 1 :在函数参数中乱用表达式作为默认值
Python 允许给一个函数的某个参数设置默认值以使该参数成为一个可选参数。尽管这是这门语言很棒的一个功能,但是这当这个默认值是可变对象( mutable )时,那就有些麻烦了。例如,看下面这个 Python 函数定义:
常见错误 1 :在函数参数中乱用表达式作为默认值
Python 允许给一个函数的某个参数设置默认值以使该参数成为一个可选参数。尽管这是这门语言很棒的一个功能,但是这当这个默认值是可变对象( mutable )时,那就有些麻烦了。例如,看下面这个 Python 函数定义:
https://uqer.io/community/share/57218746228e5b633c7b93ee
人们常犯的一个错误是认为每次调用这个函数时不给这个可选参数赋值的话,它总是会被赋予这个默认表达式的值。例如,在上面的代码中,程序员可能会认为重复调用函数 foo() (不传参数 bar 给这个函数),这个函数会总是返回‘ baz ’,因为我们假定认为每次调用 foo()的时候(不传 bar ),参数 bar 会被置为[](即,一个空的列表)。
那么我们来看看这么做的时候究竟会发生什么:
https://uqer.io/community/share/57218746228e5b633c7b93ee
嗯?为什么每次调用 foo()的时候,这个函数总是在一个已经存在的列表后面添加我们的默认值“ baz ”,而不是每次都创建一个新的列表?
答案是一个函数参数的默认值,仅仅在该函数定义的时候,被赋值一次。如此,只有当函数 foo()第一次被定义的时候,才讲参数 bar 的默认值初始化到它的默认值(即一个空的列表)。当调用 foo()的时候(不给参数 bar ),会继续使用 bar 最早初始化时的那个列表。
由此,可以有如下的解决办法:
https://uqer.io/community/share/57218746228e5b633c7b93ee
常见错误 2 :不正确的使用类变量
看下面一个例子:
https://uqer.io/community/share/57218746228e5b633c7b93ee
看起来没有问题。
https://uqer.io/community/share/57218746228e5b633c7b93ee
嗯哈,还是和预想的一样。
https://uqer.io/community/share/57218746228e5b633c7b93ee
我了个去。只是改变了 A.x ,为啥 C.x 也变了?
在 Python 里,类变量通常在内部被当做字典来处理并遵循通常所说的方法解析顺序( Method Resolution Order (MRO))。因此在上面的代码中,因为属性 x 在类 C 中找不到,因此它会往上去它的基类中查找(在上面的例子中只有 A 这个类,当然 Python 是支持多重继承( multiple inheritance )的)。换句话说, C 没有它自己独立于 A 的属性 x 。因此对 C.x 的引用实际上是对 A.x 的引用。( B.x 不是对 A.x 的引用是因为在第二步里 B.x=2 将 B.x 引用到了 2 这个对象上,倘若没有如此, B.x 仍然是引用到 A.x 上的。——译者注)
常见错误 3 :在异常处理时错误的使用参数
假设你有如下的代码:
https://uqer.io/community/share/57218746228e5b633c7b93ee
这里的问题在于 except 语句不会像这样去接受一系列的异常。并且,在 Python 2.x 里面,语法 except Exception, e 是用来将异常和这个可选的参数绑定起来(即这里的 e ),以用来在后面查看的。因此,在上面的代码中, IndexError 异常不会被 except 语句捕捉到;而最终 ValueError 这个异常被绑定在了一个叫做 IndexError 的参数上。
在 except 语句中捕捉多个异常的正确做法是将所有想要捕捉的异常放在一个元组( tuple )里并作为第一个参数给 except 语句。并且,为移植性考虑,使用 as 关键字,因为 Python 2 和 Python 3 都支持这样的语法,例如:
https://uqer.io/community/share/57218746228e5b633c7b93ee
常见错误 4 :误解 Python 作用域的规则
Python 的作用域解析是基于叫做 LEGB ( Local (本地), Enclosing (封闭), Global (全局), Built-in (内置))的规则进行操作的。这看起来很直观,对吧?事实上,在 Python 中这有一些细微的地方很容易出错。看这个例子:
https://uqer.io/community/share/57218746228e5b633c7b93ee
这是怎么回事?
这是因为,在一个作用域里面给一个变量赋值的时候, Python 自动认为这个变量是这个作用域的本地变量,并屏蔽作用域外的同名的变量。
很多时候可能在一个函数里添加一个赋值的语句会让你从前本来工作的代码得到一个 UnboundLocalError 。(感兴趣的话可以读一读这篇文章。
https://docs.python.org/2/faq/programming.html#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value )
在使用列表( lists )的时候,这种情况尤为突出。看下面这个例子:
https://uqer.io/community/share/57218746228e5b633c7b93ee
嗯?为什么 foo2 有问题,而 foo1 没有问题?
答案和上一个例子一样,但是更加不易察觉。 foo1 并没有给 lst 赋值,但是 foo2 尝试给 lst 赋值。注意 lst+=[5]只是 lst=lst+[5]的简写,由此可以看到我们尝试给 lst 赋值(因此 Python 假设作用域为本地)。但是,这个要赋给 lst 的值是基于 lst 本身的(这里的作用域仍然是本地),而 lst 却没有被定义,这就出错了。
常见错误 5 :在遍历列表的同时又在修改这个列表
下面这个例子中的代码应该比较明显了:
https://uqer.io/community/share/57218746228e5b633c7b93ee
遍历一个列表或者数组的同时又删除里面的元素,对任何有经验的软件开发人员来说这是个很明显的错误。但是像上面的例子那样明显的错误,即使有经验的程序员也可能不经意间在更加复杂的程序中不小心犯错。
所幸, Python 集成了一些优雅的编程范式,如果使用得当,可以写出相当简化和精简的代码。一个附加的好处是更简单的代码更不容易遇到这种“不小心在遍历列表时删掉列表元素”的 bug 。例如列表推导式( list comprehensions )就提供了这样的范式。再者,列表推导式在避免这样的问题上特别有用,接下来这个对上面的代码的重新实现就相当完美:
https://uqer.io/community/share/57218746228e5b633c7b93ee
常见错误 6 :搞不清楚在闭包( closures )中 Python 是怎样绑定变量的
看这个例子:
https://uqer.io/community/share/57218746228e5b633c7b93ee
期望得到下面的输出: 0 2 4 6 8
意外吧!
这是由于 Python 的后期绑定( late binding )机制导致的,这是指在闭包中使用的变量的值,是在内层函数被调用的时候查找的。因此在上面的代码中,当任一返回函数被调用的时候, i 的值是在它被调用时的周围作用域中查找(到那时,循环已经结束了,所以 i 已经被赋予了它最终的值 4 )。
解决的办法比较巧妙:
https://uqer.io/community/share/57218746228e5b633c7b93ee
这下对了!这里利用了默认参数去产生匿名函数以达到期望的效果。有人会说这很优美,有人会说这很微妙,也有人会觉得反感。但是如果你是一名 Python 程序员,重要的是能理解任何的情况。
常见错误 7 :循环加载模块
假设你有两个文件, a.py 和 b.py ,在这两个文件中互相加载对方,例如:
在 a.py 中:
https://uqer.io/community/share/57218746228e5b633c7b93ee
首先,我们试着加载 a.py :
https://uqer.io/community/share/57218746228e5b633c7b93ee
没有问题。也许让人吃惊,毕竟有个感觉应该是问题的循环加载在这儿。
事实上在 Python 中仅仅是表面上的出现循环加载并不是什么问题。如果一个模块以及被加载了, Python 不会傻到再去重新加载一遍。但是,当每个模块都想要互相访问定义在对方里的函数或者变量时,问题就来了。
让我们再回到之前的例子,当我们加载 a.py 时,它再加载 b.py 不会有问题,因为在加载 b.py 时,它并不需要访问 a.py 的任何东西,而在 b.py 中唯一的引用就是调用 a.f()。但是这个调用是在函数 g()中完成的,并且 a.py 或者 b.py 中没有人调用 g(),所以这会儿心情还是美丽的。
但是当我们试图加载 b.py 时(之前没有加载 a.py ),会发生什么呢:
https://uqer.io/community/share/57218746228e5b633c7b93ee
恭喜你,出错了。这里问题出在加载 b.py 的过程中, Python 试图加载 a.py ,并且在 a.py 中需要调用到 f(),而函数 f()又要访问到 b.x ,但是这个时候 b.x 却还没有被定义。这就产生了 AttributeError 异常。
解决的方案可以做一点细微的改动。改一下 b.py ,使得它在 g()里面加载 a.py :
https://uqer.io/community/share/57218746228e5b633c7b93ee
这会儿当我们加载 b.py 的时候,一切安好
https://uqer.io/community/share/57218746228e5b633c7b93ee
常见错误 8 :与 Python 标准库模块命名冲突
Python 的一个优秀的地方在于它提供了丰富的库模块。但是这样的结果是,如果你不下意识的避免,很容易你会遇到你自己的模块的名字与某个随 Python 附带的标准库的名字冲突的情况(比如,你的代码中可能有一个叫做 email.py 的模块,它就会与标准库中同名的模块冲突)。
这会导致一些很粗糙的问题,例如当你想加载某个库,这个库需要加载 Python 标准库里的某个模块,结果呢,因为你有一个与标准库里的模块同名的模块,这个包错误的将你的模块加载了进去,而不是加载 Python 标准库里的那个模块。这样一来就会有麻烦了。
所以在给模块起名字的时候要小心了,得避免与 Python 标准库中的模块重名。相比起你提交一个“ Python 改进建议( Python Enhancement Proposal (PEP))
http://legacy.python.org/dev/peps/”去向上要求改一个标准库里包的名字,并得到批准来说,你把自己的那个模块重新改个名字要简单得多。
常见错误 9 :不能区分 Python 2 和 Python 3
看下面这个文件 foo.py :
https://uqer.io/community/share/57218746228e5b633c7b93ee
这是怎么回事?“问题”在于,在 Python 3 里,在 except 块的作用域以外,异常对象( exception object )是不能被访问的。(原因在于,如果不这样的话, Python 会在内存的堆栈里保持一个引用链直到 Python 的垃圾处理将这些引用从内存中清除掉。更多的技术细节可以参考这里。
https://docs.python.org/3/reference/compound_stmts.html#except )
避免这样的问题可以这样做:保持在 execpt 块作用域以外对异常对象的引用,这样是可以访问的。下面是用这个办法对之前的例子做的改动,这样在 Python 2 和 Python 3 里面都运行都没有问题。
常见错误 10 :错误的使用 del 方法
假设有一个文件 mod.py 中这样使用:
https://uqer.io/community/share/57218746228e5b633c7b93ee
那么你会得到一个恶心的 AttributeError 异常。
为啥呢?这是因为(参考这里
https://mail.python.org/pipermail/python-bugs-list/2009-January/069209.html ),当解释器关闭时,模块所有的全局变量会被置为空( None )。结果便如上例所示,当__del__被调用时,名字 foo 已经被置为空了。
使用 atexit.register()可以解决这个问题。如此,当你的程序结束的时候(退出的时候),你的注册的处理程序会在解释器关闭之前处理。
这样理解的话,对上面的 mod.py 可以做如下的修改:
这样的实现方式为在程序正常终止时调用清除功能提供了一种干净可靠的办法。显然,需要 foo.cleanup 决定怎么处理绑定在 self.myhandle 上的对象,但你知道怎么做的。
总结
Python 是一门非常强大且灵活的语言,它众多的机制和范式能显著的提高生产效率。不过,和任何一款软件或者语言一样,对它的理解或认识不足的话,常常是弊大于利的,并会处于一种“一知半解”的状态。
多熟悉 Python 的一些关键的细微的地方,比如(但不局限于)本文中提到的这些问题,可以帮你更好的使用这门语言的同时帮你避免一些常见的陷阱。
感兴趣的话可以读一读这篇“ Python 面试指南( Insider ’ s Guide to Python Interviewing )
http://www.toptal.com/python#hiring-guide ”,了解一些能够区分 Python 程序员的面试题目。
希望您能在本文学到有用的地方,并欢迎您的反馈。