Python 措施员常常犯的 10 个错误
关于Python
Python是一种表明性、面向工具并具有动态语义的高级措施语言。它内建了高级的数据布局,团结了动态范例和动态绑定的利益,这使得它在快速应用开拓中很是有吸引力,而且可作为剧本或胶水语言来毗连现有的组件或处事。Python支持模块和包,从而勉励了措施的模块化和代码重用。
关于这篇文章
Python简朴易学的语法大概会使Python开拓者–尤其是那些编程的初学者–忽视了它的一些微妙的处所并低估了这门语言的本领。
有鉴于此,本文列出了一个“10强”名单,列举了甚至是高级Python开拓人员有时也难以捕获的错误。
常见错误 1: 滥用表达式作为函数参数的默认值
Python答允为函数的参数提供默认的可选值。尽量这是语言的一大特色,可是它大概会导致一些易变默认值的杂乱。譬喻,看一下这个Python函数的界说:
>>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar
一个常见的错误是认为在函数每次不提供可选参数挪用时可选参数将配置为默认指定值。在上面的代码中,譬喻,人们大概会但愿重复(即不明晰指定bar参数)地挪用foo()时总返回'baz',由于每次foo()挪用时都假定(不设定bar参数)bar被配置为[](即一个空列表)。
可是让我们看一下这样做时毕竟会产生什么:
>>> foo() ["baz"]>>> foo() ["baz", "baz"]>>> foo() ["baz", "baz", "baz"]
耶?为什么每次foo()挪用时都要把默认值"baz"追加到现有列表中而不是建设一个新的列表呢?
谜底是函数参数的默认值只会评估利用一次—在函数界说的时候。因此,bar参数在初始化时为其默认值(即一个空列表),即foo()首次界说的时候,但当挪用foo()时(即,不指定bar参数时)将继承利用bar原本已经初始化的参数。
下面是一个常见的办理要领:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]
常见错误 2: 错误地利用类变量
思量一下下面的例子:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print A.x, B.x, C.x 1 1 1
通例用一下。
>>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1
嗯,再试一下也一样。
>>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3
什么 $%#!&?? 我们只改了A.x,为什么C.x也改了?
在Python中,类变量在内部当做字典来处理惩罚,其遵循常被引用的要领理会顺序(MRO)。所以在上面的代码中,由于class C中的x属性没有找到,它会向上找它的基类(尽量Python支持多重担任,但上面的例子中只有A)。换句话说,class C中没有它本身的x属性,其独立于A。因此,C.x事实上是A.x的引用。
常见错误 3: 为 except 指定错误的参数
假设你有如下一段代码:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range
#p#分页标题#e#
这里的问题在于 except 语句并不接管以这种方法指定的异常列表。相反,在Python 2.x中,利用语法 except Exception, e 是将一个异常工具绑定到第二个可选参数(在这个例子中是 e)上,以便在后头利用。所以,在上面这个例子中,IndexError 这个异常并不是被except语句捕获到的,而是被绑定到一个名叫 IndexError的参数上时激发的。
在一个except语句中捕捉多个异常的正确做法是将第一个参数指定为一个含有所有要捕捉异常的元组。而且,为了代码的可移植性,要利用as要害词,因为Python 2 和Python 3都支持这种语法:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
常见错误 4: 不领略Python的浸染域
Python是基于 LEGB 来举办浸染于理会的, LEGB 是 Local, Enclosing, Global, Built-in 的缩写。看起来“见文知意”,对吗?实际上,在Python中尚有一些需要留意的处所,先看下面一段代码:
>>> 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 时,这个问题就越发常见. 请看下面这个例子:
>>> 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 += [5] 是 lst = lst + [5] 的缩写,我们试图对 lst 举办赋值操纵(Python把他当成结局部变量)。另外,我们对 lst 举办的赋值操纵是基于 lst 自身(这再一次被Python当成结局部变量),但此时还未界说。因此堕落!
常见错误 5:当迭代时修改一个列表(List)
下面代码中的问题应该是相当明明的:
>>> 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] # BAD: Deleting item from a list while iterating over it ...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: list index out of range
#p#分页标题#e#
当迭代的时候,从一个 列表 (List)可能数组中删除元素,对付任何有履历的开拓者来说,这是一个众所周知的错误。尽量上面的例子很是明明,可是很多高级开拓者在更巨大的代码中也并非是存心而为之的。
幸运的是,Python包括大量简捷优雅的编程典型,若利用恰当,能大大简化和精辟代码。这样的长处是能获得更简化和更精简的代码,能更好的制止措施中呈现当迭代时修改一个列表(List)这样的bug。一个这样的典型是递推式列表(list comprehensions)。并且,递推式列表(list comprehensions)针对这个问题是出格有用的,通过变动上文中的实现,获得一段极佳的代码:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]
常见错误 6: 不大白Python在闭包中是如何绑定变量的
看下面这个例子:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
你也许但愿得到下面的输出功效:
0
2
4
6
8
但实际的功效却是:
8
8
8
8
8
惊奇吧!
这之所以会产生是由于Python中的“后期绑定”行为——闭包顶用到的变量只有在函数被挪用的时候才会被赋值。所以,在上面的代码中,任何时候,当返回的函数被挪用时,Python会在该函数被挪用时的浸染域中查找 i 对应的值(这时,轮回已经竣事,所以 i 被赋上了最终的值——4)。
办理的要领有一点hack的味道:
>>> 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 开拓者,领略这种行为很重要。
常见错误 7: 建设轮回依赖模块
让我们假设你有两个文件,a.py 和 b.py,他们之间彼此引用,如下所示:
a.py:
import b def f(): return b.x print f()
b.py:
import a x = 1 def g(): print a.f()
首先,让我们实验引入 a.py:
>>> import a
1
可以正常事情。这也许是你感想很奇怪。究竟,我们确实在这里引入了一个轮回依赖的模块,我们猜测这样会出问题的,不是吗?
谜底就是在Python中,仅仅引入一个轮回依赖的模块是没有问题的。假如一个模块已经被引入了,Python并不会去再次引入它。可是,按照每个模块要会见其他模块中的函数和变量位置的差异,就很大概会碰着问题。
所以,回到我们这个例子,当我们引入 a.py 时,再引入 b.py 不会发生任何问题,因为当引入的时候,b.py 不需要 a.py 中界说任何对象。b.py 中独一引用 a.py 中的对象是挪用 a.f()。 可是谁人挪用是产生在g() 中的,而且 a.py 和 b.py 中都没有挪用 g()。所以运行正常。
可是,假如我们实验去引入b.py 会产生什么呢?(在这之前不引入a.py),如下所示:
>>> 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'
#p#分页标题#e#
啊哦。 出问题了!此处的问题是,在引入b.py的进程中,Python实验去引入 a.py,可是a.py 要挪用f(),而f() 有实验去会见 b.x。可是此时 b.x 还没有被界说呢。所以产生了 AttributeError 异常。
至少,办理这个问题很简朴,只需修改b.py,使其在g()中引入 a.py:
x = 1 def g(): import a # 只有当g()被挪用的时候才会引入a print a.f()
此刻,当我们再引入b,没有任何问题:
>>> import b >>> b.g() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'
常见错误 8: 与Python尺度库中的模块定名斗嘴
Python一个令人传颂的处所是它有富厚的模块可供我们“开箱即用”。可是,假如你没有有意识的留意的话,就很容易呈现你写的模块和Python自带的尺度库的模块之间产生定名斗嘴的问题(如,你也许有一个叫 email.py 的模块,但这会和尺度库中的同名模块斗嘴)。
这大概会导致很怪的问题,譬喻,你引入了另一个模块,但这个模块要引入一个Python尺度库中的模块,由于你界说了一个同名的模块,就会使该模块错误的引入了你的模块,而不是 stdlib 中的模块。这就会出问题了。
因此,我们必需要留意这个问题,以制止利用和Python尺度库中沟通的模块名。修改你包中的模块名要比通过 Python Enhancement Proposal (PEP) 给Python提发起来修改尺度库的模块名容易多了。
常见错误 #9: 未能办理Python 2和Python 3之间的差别
请看下面这个 filefoo.py:
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中运行正常:
$ python foo.py 1
key error
1
$ python foo.py 2
value error
2
可是,此刻让我们把它在Python 3中运行一下:
$ 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 代码块之外是不行见的。(这样做的原因是,它将生存一个对内存中仓库帧的引用周期,直到垃圾接纳器运行而且从内存中排除去引用。相识更多技能细节请参考这里) 。
一种办理步伐是在 except 代码块的外部浸染域中界说一个对异常工具的引用,以便会见。下面的例子利用了该要领,因此最后的代码可以在Python 2 和 Python 3中运行精采。
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中运行:
$ python3 foo.py 1
key error
1
$ python3 foo.py 2
value error
2
正常!
(顺便提一下, 我们的 Python Hiring Guide 接头了当我们把代码从Python 2 迁移到 Python 3时的其他一些需要知道的重要差别。)
常见错误 10: 误用__del__要领
#p#分页标题#e#
假设你有一个名为 calledmod.py 的文件:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
而且有一个名为 another_mod.py 的文件:
import mod
mybar = mod.Bar()
你会获得一个 AttributeError 的异常。
为什么呢?因为,正如这里所说,当表明器退出的时候,模块中的全局变量都被配置成了 None。所以,在上面这个例子中,当 __del__ 被挪用时,foo 已经被配置成了None。
办理要领是利用 atexit.register() 取代。用这种方法,当你的措施竣事执行时(意思是正常退出),你注册的处理惩罚措施会在表明器退出之前执行。
相识了这些,我们可以将上面 mod.py 的代码修改成下面的这样:
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是一门强大的而且很机动的语言,它有许多机制和语言类型来显著的提高你的出产力。和其他任何一门语言或软件一样,假如对它本领的相识有限,这很大概会给你带来阻碍,而不是长处。正如一句谚语所说的那样 “knowing enough to be dangerous”(译者注:意思是自觉得已经相识足够了,可以做某事了,但其实不是)。
熟悉Python的一些要害的细微之处,像本文中所提到的那些(但不限于这些),可以辅佐我们更好的去利用语言,从而制止一些常见的陷阱。