Python 程序員經(jīng)常犯的 10 個(gè)錯(cuò)誤
關(guān)于Python
Python是一種解釋性、面向?qū)ο蟛⒕哂袆?dòng)態(tài)語(yǔ)義的高級(jí)程序語(yǔ)言。它內(nèi)建了高級(jí)的數(shù)據(jù)結(jié)構(gòu),結(jié)合了動(dòng)態(tài)類型和動(dòng)態(tài)綁定的優(yōu)點(diǎn),這使得它在快速應(yīng)用開發(fā)中非常有吸引力,并且可作為腳本或膠水語(yǔ)言來連接現(xiàn)有的組件或服務(wù)。Python支持模塊和包,從而鼓勵(lì)了程序的模塊化和代碼重用。
關(guān)于這篇文章
Python簡(jiǎn)單易學(xué)的語(yǔ)法可能會(huì)使Python開發(fā)者–尤其是那些編程的初學(xué)者–忽視了它的一些微妙的地方并低估了這門語(yǔ)言的能力。
有鑒于此,本文列出了一個(gè)“10強(qiáng)”名單,枚舉了甚至是高級(jí)Python開發(fā)人員有時(shí)也難以捕捉的錯(cuò)誤。
常見錯(cuò)誤 #1: 濫用表達(dá)式作為函數(shù)參數(shù)的默認(rèn)值
Python允許為函數(shù)的參數(shù)提供默認(rèn)的可選值。盡管這是語(yǔ)言的一大特色,但是它可能會(huì)導(dǎo)致一些易變默認(rèn)值的混亂。例如,看一下這個(gè)Python函數(shù)的定義:
- >>> 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
一個(gè)常見的錯(cuò)誤是認(rèn)為在函數(shù)每次不提供可選參數(shù)調(diào)用時(shí)可選參數(shù)將設(shè)置為默認(rèn)指定值。在上面的代碼中,例如,人們可能會(huì)希望反復(fù)(即不明確指定bar參數(shù))地調(diào)用foo()時(shí)總返回'baz',由于每次foo()調(diào)用時(shí)都假定(不設(shè)定bar參數(shù))bar被設(shè)置為[](即一個(gè)空列表)。
但是讓我們看一下這樣做時(shí)究竟會(huì)發(fā)生什么:
- >>> foo()
- ["baz"]>>> foo()
- ["baz", "baz"]>>> foo()
- ["baz", "baz", "baz"]
耶?為什么每次foo()調(diào)用時(shí)都要把默認(rèn)值"baz"追加到現(xiàn)有列表中而不是創(chuàng)建一個(gè)新的列表呢?
答案是函數(shù)參數(shù)的默認(rèn)值只會(huì)評(píng)估使用一次—在函數(shù)定義的時(shí)候。因此,bar參數(shù)在初始化時(shí)為其默認(rèn)值(即一個(gè)空列表),即foo()***定義的時(shí)候,但當(dāng)調(diào)用foo()時(shí)(即,不指定bar參數(shù)時(shí))將繼續(xù)使用bar原本已經(jīng)初始化的參數(shù)。
下面是一個(gè)常見的解決方法:
- >>> def foo(bar=None):
- ... if bar is None: # or if not bar:
- ... bar = []
- ... bar.append("baz")
- ... return bar
- ...
- >>> foo()
- ["baz"]
- >>> foo()
- ["baz"]
- >>> foo()
- ["baz"]
常見錯(cuò)誤 #2: 錯(cuò)誤地使用類變量
考慮一下下面的例子:
- >>> class A(object):
- ... x = 1
- ...
- >>> class B(A):
- ... pass
- ...
- >>> class C(A):
- ... pass
- ...
- >>> print A.x, B.x, C.x
- 1 1 1
常規(guī)用一下。
- >>> 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中,類變量在內(nèi)部當(dāng)做字典來處理,其遵循常被引用的方法解析順序(MRO)。所以在上面的代碼中,由于class C中的x屬性沒有找到,它會(huì)向上找它的基類(盡管Python支持多重繼承,但上面的例子中只有A)。換句話說,class C中沒有它自己的x屬性,其獨(dú)立于A。因此,C.x事實(shí)上是A.x的引用。
常見錯(cuò)誤 #3: 為 except 指定錯(cuò)誤的參數(shù)
假設(shè)你有如下一段代碼:
- >>> 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
這里的問題在于 except 語(yǔ)句并不接受以這種方式指定的異常列表。相反,在Python 2.x中,使用語(yǔ)法 except Exception, e 是將一個(gè)異常對(duì)象綁定到第二個(gè)可選參數(shù)(在這個(gè)例子中是 e)上,以便在后面使用。所以,在上面這個(gè)例子中,IndexError 這個(gè)異常并不是被except語(yǔ)句捕捉到的,而是被綁定到一個(gè)名叫 IndexError的參數(shù)上時(shí)引發(fā)的。
在一個(gè)except語(yǔ)句中捕獲多個(gè)異常的正確做法是將***個(gè)參數(shù)指定為一個(gè)含有所有要捕獲異常的元組。并且,為了代碼的可移植性,要使用as關(guān)鍵詞,因?yàn)镻ython 2 和Python 3都支持這種語(yǔ)法:
- >>> try:
- ... l = ["a", "b"]
- ... int(l[2])
- ... except (ValueError, IndexError) as e:
- ... pass
- ...
- >>>
常見錯(cuò)誤 #4: 不理解Python的作用域
Python是基于 LEGB 來進(jìn)行作用于解析的, LEGB 是 Local, Enclosing, Global, Built-in 的縮寫。看起來“見文知意”,對(duì)嗎?實(shí)際上,在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
這里出什么問題了?
上面的問題之所以會(huì)發(fā)生是因?yàn)楫?dāng)你給作用域中的一個(gè)變量賦值時(shí),Python 會(huì)自動(dòng)的把它當(dāng)做是當(dāng)前作用域的局部變量,從而會(huì)隱藏外部作用域中的同名變量。
很多人會(huì)感到很吃驚,當(dāng)他們給之前可以正常運(yùn)行的代碼的函數(shù)體的某個(gè)地方添加了一句賦值語(yǔ)句之后就得到了一個(gè) UnboundLocalError 的錯(cuò)誤。 (你可以在這里了解到更多)
尤其是當(dāng)開發(fā)者使用 lists 時(shí),這個(gè)問題就更加常見. 請(qǐng)看下面這個(gè)例子:
- >>> 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 報(bào)錯(cuò),而foo1沒有問題呢?
原因和之前那個(gè)例子的一樣,不過更加令人難以捉摸。foo1 沒有對(duì) lst 進(jìn)行賦值操作,而 foo2 做了。要知道, lst += [5] 是 lst = lst + [5] 的縮寫,我們?cè)噲D對(duì) lst 進(jìn)行賦值操作(Python把他當(dāng)成了局部變量)。此外,我們對(duì) lst 進(jìn)行的賦值操作是基于 lst 自身(這再一次被Python當(dāng)成了局部變量),但此時(shí)還未定義。因此出錯(cuò)!
常見錯(cuò)誤#5:當(dāng)?shù)鷷r(shí)修改一個(gè)列表(List)
下面代碼中的問題應(yīng)該是相當(dāng)明顯的:
- >>> 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
當(dāng)?shù)臅r(shí)候,從一個(gè) 列表 (List)或者數(shù)組中刪除元素,對(duì)于任何有經(jīng)驗(yàn)的開發(fā)者來說,這是一個(gè)眾所周知的錯(cuò)誤。盡管上面的例子非常明顯,但是許多高級(jí)開發(fā)者在更復(fù)雜的代碼中也并非是故意而為之的。
幸運(yùn)的是,Python包含大量簡(jiǎn)潔優(yōu)雅的編程范例,若使用得當(dāng),能大大簡(jiǎn)化和精煉代碼。這樣的好處是能得到更簡(jiǎn)化和更精簡(jiǎn)的代碼,能更好的避免程序中出現(xiàn)當(dāng)?shù)鷷r(shí)修改一個(gè)列表(List)這樣的bug。一個(gè)這樣的范例是遞推式列表(list comprehensions)。而且,遞推式列表(list comprehensions)針對(duì)這個(gè)問題是特別有用的,通過更改上文中的實(shí)現(xiàn),得到一段***的代碼:
- >>> 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]
常見錯(cuò)誤 #6: 不明白Python在閉包中是如何綁定變量的
看下面這個(gè)例子:
- >>> def create_multipliers():
- ... return [lambda x : i * x for i in range(5)]
- >>> for multiplier in create_multipliers():
- ... print multiplier(2)
- ...
你也許希望獲得下面的輸出結(jié)果:
- 0
- 2
- 4
- 6
- 8
但實(shí)際的結(jié)果卻是:
- 8
- 8
- 8
- 8
- 8
驚訝吧!
這之所以會(huì)發(fā)生是由于Python中的“后期綁定”行為——閉包中用到的變量只有在函數(shù)被調(diào)用的時(shí)候才會(huì)被賦值。所以,在上面的代碼中,任何時(shí)候,當(dāng)返回的函數(shù)被調(diào)用時(shí),Python會(huì)在該函數(shù)被調(diào)用時(shí)的作用域中查找 i 對(duì)應(yīng)的值(這時(shí),循環(huán)已經(jīng)結(jié)束,所以 i 被賦上了最終的值——4)。
解決的方法有一點(diǎn)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
在這里,我們利用了默認(rèn)參數(shù)來生成一個(gè)匿名的函數(shù)以便實(shí)現(xiàn)我們想要的結(jié)果。有人說這個(gè)方法很巧妙,有人說它難以理解,還有人討厭這種做法。但是,如果你是一個(gè) Python 開發(fā)者,理解這種行為很重要。
#p#
常見錯(cuò)誤 #7: 創(chuàng)建循環(huán)依賴模塊
讓我們假設(shè)你有兩個(gè)文件,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
可以正常工作。這也許是你感到很奇怪。畢竟,我們確實(shí)在這里引入了一個(gè)循環(huán)依賴的模塊,我們推測(cè)這樣會(huì)出問題的,不是嗎?
答案就是在Python中,僅僅引入一個(gè)循環(huán)依賴的模塊是沒有問題的。如果一個(gè)模塊已經(jīng)被引入了,Python并不會(huì)去再次引入它。但是,根據(jù)每個(gè)模塊要訪問其他模塊中的函數(shù)和變量位置的不同,就很可能會(huì)遇到問題。
所以,回到我們這個(gè)例子,當(dāng)我們引入 a.py 時(shí),再引入 b.py 不會(huì)產(chǎn)生任何問題,因?yàn)楫?dāng)引入的時(shí)候,b.py 不需要 a.py 中定義任何東西。b.py 中唯一引用 a.py 中的東西是調(diào)用 a.f()。 但是那個(gè)調(diào)用是發(fā)生在g() 中的,并且 a.py 和 b.py 中都沒有調(diào)用 g()。所以運(yùn)行正常。
但是,如果我們嘗試去引入b.py 會(huì)發(fā)生什么呢?(在這之前不引入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'
啊哦。 出問題了!此處的問題是,在引入b.py的過程中,Python嘗試去引入 a.py,但是a.py 要調(diào)用f(),而f() 有嘗試去訪問 b.x。但是此時(shí) b.x 還沒有被定義呢。所以發(fā)生了 AttributeError 異常。
至少,解決這個(gè)問題很簡(jiǎn)單,只需修改b.py,使其在g()中引入 a.py:
- x = 1
- def g():
- import a # 只有當(dāng)g()被調(diào)用的時(shí)候才會(huì)引入a
- print a.f()
現(xiàn)在,當(dāng)我們?cè)僖隻,沒有任何問題:
- >>> 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'
常見錯(cuò)誤 #8: 與Python標(biāo)準(zhǔn)庫(kù)中的模塊命名沖突
Python一個(gè)令人稱贊的地方是它有豐富的模塊可供我們“開箱即用”。但是,如果你沒有有意識(shí)的注意的話,就很容易出現(xiàn)你寫的模塊和Python自帶的標(biāo)準(zhǔn)庫(kù)的模塊之間發(fā)生命名沖突的問題(如,你也許有一個(gè)叫 email.py 的模塊,但這會(huì)和標(biāo)準(zhǔn)庫(kù)中的同名模塊沖突)。
這可能會(huì)導(dǎo)致很怪的問題,例如,你引入了另一個(gè)模塊,但這個(gè)模塊要引入一個(gè)Python標(biāo)準(zhǔn)庫(kù)中的模塊,由于你定義了一個(gè)同名的模塊,就會(huì)使該模塊錯(cuò)誤的引入了你的模塊,而不是 stdlib 中的模塊。這就會(huì)出問題了。
因此,我們必須要注意這個(gè)問題,以避免使用和Python標(biāo)準(zhǔn)庫(kù)中相同的模塊名。修改你包中的模塊名要比通過 Python Enhancement Proposal (PEP) 給Python提建議來修改標(biāo)準(zhǔn)庫(kù)的模塊名容易多了。
常見錯(cuò)誤 #9: 未能解決Python 2和Python 3之間的差異
請(qǐng)看下面這個(gè) 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中運(yùn)行正常:
- $ python foo.py 1
- key error
- 1
- $ python foo.py 2
- value error
- 2
但是,現(xiàn)在讓我們把它在Python 3中運(yùn)行一下:
- $ 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 中,異常的對(duì)象在 except 代碼塊之外是不可見的。(這樣做的原因是,它將保存一個(gè)對(duì)內(nèi)存中堆棧幀的引用周期,直到垃圾回收器運(yùn)行并且從內(nèi)存中清除掉引用。了解更多技術(shù)細(xì)節(jié)請(qǐng)參考這里) 。
一種解決辦法是在 except 代碼塊的外部作用域中定義一個(gè)對(duì)異常對(duì)象的引用,以便訪問。下面的例子使用了該方法,因此***的代碼可以在Python 2 和 Python 3中運(yùn)行良好。
- 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中運(yùn)行:
- $ python3 foo.py 1
- key error
- 1
- $ python3 foo.py 2
- value error
- 2
正常!
(順便提一下, 我們的 Python Hiring Guide 討論了當(dāng)我們把代碼從Python 2 遷移到 Python 3時(shí)的其他一些需要知道的重要差異。)
常見錯(cuò)誤 #10: 誤用__del__方法
假設(shè)你有一個(gè)名為 calledmod.py 的文件:
- import foo
- class Bar(object):
- ...
- def __del__(self):
- foo.cleanup(self.myhandle)
并且有一個(gè)名為 another_mod.py 的文件:
- import mod
- mybar = mod.Bar()
你會(huì)得到一個(gè) AttributeError 的異常。
為什么呢?因?yàn)椋?a rel="nofollow" target="_blank" >這里所說,當(dāng)解釋器退出的時(shí)候,模塊中的全局變量都被設(shè)置成了 None。所以,在上面這個(gè)例子中,當(dāng) __del__ 被調(diào)用時(shí),foo 已經(jīng)被設(shè)置成了None。
解決方法是使用 atexit.register() 代替。用這種方式,當(dāng)你的程序結(jié)束執(zhí)行時(shí)(意思是正常退出),你注冊(cè)的處理程序會(huì)在解釋器退出之前執(zhí)行。
了解了這些,我們可以將上面 mod.py 的代碼修改成下面的這樣:
- import foo
- import atexit
- def cleanup(handle):
- foo.cleanup(handle)
- class Bar(object):
- def __init__(self):
- ...
- atexit.register(cleanup, self.myhandle)
這種實(shí)現(xiàn)方式提供了一個(gè)整潔并且可信賴的方法用來在程序退出之前做一些清理工作。很顯然,它是由foo.cleanup 來決定對(duì)綁定在 self.myhandle 上對(duì)象做些什么處理工作的,但是這就是你想要的。
總結(jié)
Python是一門強(qiáng)大的并且很靈活的語(yǔ)言,它有很多機(jī)制和語(yǔ)言規(guī)范來顯著的提高你的生產(chǎn)力。和其他任何一門語(yǔ)言或軟件一樣,如果對(duì)它能力的了解有限,這很可能會(huì)給你帶來阻礙,而不是好處。正如一句諺語(yǔ)所說的那樣 “knowing enough to be dangerous”(譯者注:意思是自以為已經(jīng)了解足夠了,可以做某事了,但其實(shí)不是)。
熟悉Python的一些關(guān)鍵的細(xì)微之處,像本文中所提到的那些(但不限于這些),可以幫助我們更好的去使用語(yǔ)言,從而避免一些常見的陷阱。
你可以查看“Python 面試官指南” 來獲得一些關(guān)于如何辨別一個(gè)開發(fā)者是否是Python專家的建議。
我們希望你在這篇文章中找到了一些對(duì)你有幫助的東西,并希望你得到你的反饋。
英文原文:Top 10 Mistakes that Python Programmers Make
譯文鏈接:http://www.oschina.net/translate/top-10-mistakes-that-python-programmers-make



















