Python是一门解释性的,面向对象的,并具备动态语义的高级编程语言。它高级的内置数据结构,结合其动态类型和动态绑定的特性,使得它在快速应用程序开发(Rapid Application Development)中颇为受欢迎,同时Python还能做为脚本语言或者胶水语言讲现成的组件或者服务结合起来。Python支持模块(modules)和包(packages),因此也鼓励程序的模块化以及代码重用。html
Python简单、易学的语法可能会误导一些Python程序员(特别是那些刚接触这门语言的人们),可能会忽略某些细微之处和这门语言的强大之处。python
考虑到这点,本文列出了“十大”甚至是高级的Python程序员均可能犯的,却又不容易发现的细微错误。(注意:本文是针对比《Python程序员常见错误》稍微高级一点读者,对于更加新手一点的Python程序员,有兴趣能够读一读那篇文章)程序员
Python容许给一个函数的某个参数设置默认值以使该参数成为一个可选参数。尽管这是这门语言很棒的一个功能,可是这当这个默认值是可变对象(mutable)时,那就有些麻烦了。例如,看下面这个Python函数定义:web
Python面试
1编程 2api 3数组 |
>>> def foo(bar=[]): # bar是可选参数,若是没有指明的话,默认值是[]数据结构 ... bar.append("baz") # 可是这行但是有问题的,走着瞧…闭包 ... return bar |
人们常犯的一个错误是认为每次调用这个函数时不给这个可选参数赋值的话,它老是会被赋予这个默认表达式的值。例如,在上面的代码中,程序员可能会认为重复调用函数foo() (不传参数bar给这个函数),这个函数会老是返回‘baz’,由于咱们假定认为每次调用foo()的时候(不传bar),参数bar会被置为[](即,一个空的列表)。
那么咱们来看看这么作的时候究竟会发生什么:
Python
1 2 3 4 5 6 |
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"] |
嗯?为何每次调用foo()的时候,这个函数老是在一个已经存在的列表后面添加咱们的默认值“baz”,而不是每次都建立一个新的列表?
答案是一个函数参数的默认值,仅仅在该函数定义的时候,被赋值一次。如此,只有当函数foo()第一次被定义的时候,才讲参数bar的默认值初始化到它的默认值(即一个空的列表)。当调用foo()的时候(不给参数bar),会继续使用bar最先初始化时的那个列表。
由此,能够有以下的解决办法:
Python
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> def foo(bar=None): ... if bar is None: # 或者用 if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"] |
看下面一个例子:
Python
1 2 3 4 5 6 7 8 9 10 11 |
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1 |
看起来没有问题。
Python
1 2 3 |
>>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1 |
嗯哈,仍是和预想的同样。
Python
1 2 3 |
>>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3 |
我了个去。只是改变了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上的。——译者注)
假设你有以下的代码:
Python
1 2 3 4 5 6 7 8 9 |
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # 想捕捉两个异常 ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range |
这里的问题在于except语句不会像这样去接受一系列的异常。而且,在Python 2.x里面,语法except Exception, e是用来将异常和这个可选的参数绑定起来(即这里的e),以用来在后面查看的。所以,在上面的代码中,IndexError异常不会被except语句捕捉到;而最终ValueError这个异常被绑定在了一个叫作IndexError的参数上。
在except语句中捕捉多个异常的正确作法是将全部想要捕捉的异常放在一个元组(tuple)里并做为第一个参数给except语句。而且,为移植性考虑,使用as关键字,由于Python 2和Python 3都支持这样的语法,例如:
Python
1 2 3 4 5 6 7 |
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>> |
Python的做用域解析是基于叫作LEGB(Local(本地),Enclosing(封闭),Global(全局),Built-in(内置))的规则进行操做的。这看起来很直观,对吧?事实上,在Python中这有一些细微的地方很容易出错。看这个例子:
Python
1 2 3 4 5 6 7 8 9 10 |
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment |
这是怎么回事?
这是由于,在一个做用域里面给一个变量赋值的时候,Python自动认为这个变量是这个做用域的本地变量,并屏蔽做用域外的同名的变量。
不少时候可能在一个函数里添加一个赋值的语句会让你从前原本工做的代码获得一个UnboundLocalError。(感兴趣的话能够读一读这篇文章。)
在使用列表(lists)的时候,这种状况尤其突出。看下面这个例子:
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # 这没有问题... ... >>> foo1() >>> lst [1, 2, 3, 5]
>>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... 这就有问题了! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment |
嗯?为何foo2有问题,而foo1没有问题?
答案和上一个例子同样,可是更加不易察觉。foo1并无给lst赋值,可是foo2尝试给lst赋值。注意lst+=[5]只是lst=lst+[5]的简写,由此能够看到咱们尝试给lst赋值(所以Python假设做用域为本地)。可是,这个要赋给lst的值是基于lst自己的(这里的做用域仍然是本地),而lst却没有被定义,这就出错了。
下面这个例子中的代码应该比较明显了:
Python
1 2 3 4 5 6 7 8 9 |
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # 这不对的:在遍历列表时删掉列表的元素。 ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range |
遍历一个列表或者数组的同时又删除里面的元素,对任何有经验的软件开发人员来讲这是个很明显的错误。可是像上面的例子那样明显的错误,即便有经验的程序员也可能不经意间在更加复杂的程序中不当心犯错。
所幸,Python集成了一些优雅的编程范式,若是使用得当,能够写出至关简化和精简的代码。一个附加的好处是更简单的代码更不容易遇到这种“不当心在遍历列表时删掉列表元素”的bug。例如列表推导式(list comprehensions)就提供了这样的范式。再者,列表推导式在避免这样的问题上特别有用,接下来这个对上面的代码的从新实现就至关完美:
Python
1 2 3 4 5 |
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # 啊,这多优美 >>> numbers [0, 2, 4, 6, 8] |
看这个例子:
Python
1 2 3 4 5 |
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ... |
指望获得下面的输出:
Python
1 2 3 4 5 |
0 2 4 6 8 |
可是实际上获得的是:
Python
1 2 3 4 5 |
8 8 8 8 8 |
意外吧!
这是因为Python的后期绑定(late binding)机制致使的,这是指在闭包中使用的变量的值,是在内层函数被调用的时候查找的。所以在上面的代码中,当任一返回函数被调用的时候,i的值是在它被调用时的周围做用域中查找(到那时,循环已经结束了,因此i已经被赋予了它最终的值4)。
解决的办法比较巧妙:
Python
1 2 3 4 5 6 7 8 9 10 11 |
>>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8 |
这下对了!这里利用了默认参数去产生匿名函数以达到指望的效果。有人会说这很优美,有人会说这很微妙,也有人会以为反感。可是若是你是一名Python程序员,重要的是能理解任何的状况。
假设你有两个文件,a.py和b.py,在这两个文件中互相加载对方,例如:
在a.py中:
Python
1 2 3 4 |
import b def f(): return b.x print f() |
在b.py中:
Python
1 2 3 4 |
import a x = 1 def g(): print a.f() |
首先,咱们试着加载a.py:
Python
1 2 |
>>> import a 1 |
没有问题。也许让人吃惊,毕竟有个感受应该是问题的循环加载在这儿。
事实上在Python中仅仅是表面上的出现循环加载并非什么问题。若是一个模块以及被加载了,Python不会傻到再去从新加载一遍。可是,当每一个模块都想要互相访问定义在对方里的函数或者变量时,问题就来了。
让咱们再回到以前的例子,当咱们加载a.py时,它再加载b.py不会有问题,由于在加载b.py时,它并不须要访问a.py的任何东西,而在b.py中惟一的引用就是调用a.f()。可是这个调用是在函数g()中完成的,而且a.py或者b.py中没有人调用g(),因此这会儿心情仍是美丽的。
可是当咱们试图加载b.py时(以前没有加载a.py),会发生什么呢:
Python
1 2 3 4 5 6 7 8 9 10 |
>>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return b.x AttributeError: 'module' object has no attribute 'x' |
恭喜你,出错了。这里问题出在加载b.py的过程当中,Python试图加载a.py,而且在a.py中须要调用到f(),而函数f()又要访问到b.x,可是这个时候b.x却尚未被定义。这就产生了AttributeError异常。
解决的方案能够作一点细微的改动。改一下b.py,使得它在g()里面加载a.py:
Python
1 2 3 4 |
x = 1 def g(): import a # 只有当g()被调用的时候才加载 print a.f() |
这会儿当咱们加载b.py的时候,一切安好:
Python
1 2 3 4 |
>>> import b >>> b.g() 1 # 第一次输出,由于模块a在最后调用了‘print f()’ 1 # 第二次输出,这是咱们调用g() |
Python的一个优秀的地方在于它提供了丰富的库模块。可是这样的结果是,若是你不下意识的避免,很容易你会遇到你本身的模块的名字与某个随Python附带的标准库的名字冲突的状况(好比,你的代码中可能有一个叫作email.py的模块,它就会与标准库中同名的模块冲突)。
这会致使一些很粗糙的问题,例如当你想加载某个库,这个库须要加载Python标准库里的某个模块,结果呢,由于你有一个与标准库里的模块同名的模块,这个包错误的将你的模块加载了进去,而不是加载Python标准库里的那个模块。这样一来就会有麻烦了。
因此在给模块起名字的时候要当心了,得避免与Python标准库中的模块重名。相比起你提交一个“Python改进建议(Python Enhancement Proposal (PEP))”去向上要求改一个标准库里包的名字,并获得批准来讲,你把本身的那个模块从新改个名字要简单得多。
看下面这个文件foo.py:
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import sys
def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2)
def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e)
bad() |
在Python 2里,运行起来没有问题:
1 2 3 4 5 6 |
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2 |
可是若是拿到Python 3上面玩玩:
1 2 3 4 5 6 7 8 |
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment |
这是怎么回事?“问题”在于,在Python 3里,在except块的做用域之外,异常对象(exception object)是不能被访问的。(缘由在于,若是不这样的话,Python会在内存的堆栈里保持一个引用链直到Python的垃圾处理将这些引用从内存中清除掉。更多的技术细节能够参考这里。)
避免这样的问题能够这样作:保持在execpt块做用域之外对异常对象的引用,这样是能够访问的。下面是用这个办法对以前的例子作的改动,这样在Python 2和Python 3里面都运行都没有问题。
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import sys
def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2)
def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception)
good() |
在Py3k里面运行:
1 2 3 4 5 6 |
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2 |
耶!
(顺带提一下,咱们的“Python招聘指南”里讨论了从Python 2移植代码到Python 3时须要注意的其余重要的不一样之处。)
假设有一个文件mod.py中这样使用:
Python
1 2 3 4 5 6 |
import foo
class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle) |
而后试图在another_mod.py里这样:
Python
1 2 |
import mod mybar = mod.Bar() |
那么你会获得一个恶心的AttributeError异常。
为啥呢?这是由于(参考这里),当解释器关闭时,模块全部的全局变量会被置为空(None)。结果便如上例所示,当__del__被调用时,名字foo已经被置为空了。
使用atexit.register()能够解决这个问题。如此,当你的程序结束的时候(退出的时候),你的注册的处理程序会在解释器关闭以前处理。
这样理解的话,对上面的mod.py能够作以下的修改:
Python
1 2 3 4 5 6 7 8 9 10 |
import foo import atexit
def cleanup(handle): foo.cleanup(handle)
class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle) |
这样的实现方式为在程序正常终止时调用清除功能提供了一种干净可靠的办法。显然,须要foo.cleanup决定怎么处理绑定在self.myhandle上的对象,但你知道怎么作的。
Python 是一门很是强大且灵活的语言,它众多的机制和范式能显著的提升生产效率。不过,和任何一款软件或者语言同样,对它的理解或认识不足的话,经常是弊大于利的,并会处于一种“只知其一;不知其二”的状态。
多熟悉Python的一些关键的细微的地方,好比(但不局限于)本文中提到的这些问题,能够帮你更好的使用这门语言的同时帮你避免一些常见的陷阱。
感兴趣的话能够读一读这篇“Python面试指南(Insider’s Guide to Python Interviewing)”,了解一些可以区分Python程序员的面试题目。
但愿您能在本文学到有用的地方,并欢迎您的反馈。