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)用開(kāi)發(fā)中非常有吸引力,并且可作為腳本或膠水語(yǔ)言來(lái)連接現(xiàn)有的組件或服務(wù)。Python支持模塊和包,從而鼓勵(lì)了程序的模塊化和代碼重用。
關(guān)于這篇文章
Python簡(jiǎn)單易學(xué)的語(yǔ)法可能會(huì)使Python開(kāi)發(fā)者–尤其是那些編程的初學(xué)者–忽視了它的一些微妙的地方并低估了這門(mén)語(yǔ)言的能力。
有鑒于此,本文列出了一個(gè)“10強(qiáng)”名單,枚舉了甚至是高級(jí)Python開(kāi)發(fā)人員有時(shí)也難以捕捉的錯(cuò)誤。
常見(jiàn)錯(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è)常見(jiàn)的錯(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è)常見(jiàn)的解決方法:
- >>> def foo(bar=None):
 - ... if bar is None: # or if not bar:
 - ... bar = []
 - ... bar.append("baz")
 - ... return bar
 - ...
 - >>> foo()
 - ["baz"]
 - >>> foo()
 - ["baz"]
 - >>> foo()
 - ["baz"]
 
常見(jiàn)錯(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)做字典來(lái)處理,其遵循常被引用的方法解析順序(MRO)。所以在上面的代碼中,由于class C中的x屬性沒(méi)有找到,它會(huì)向上找它的基類(盡管Python支持多重繼承,但上面的例子中只有A)。換句話說(shuō),class C中沒(méi)有它自己的x屬性,其獨(dú)立于A。因此,C.x事實(shí)上是A.x的引用。
常見(jiàn)錯(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
 
這里的問(wèn)題在于 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
 - ...
 - >>>
 
常見(jiàn)錯(cuò)誤 #4: 不理解Python的作用域
Python是基于 LEGB 來(lái)進(jìn)行作用于解析的, LEGB 是 Local, Enclosing, Global, Built-in 的縮寫(xiě)。看起來(lái)“見(jiàn)文知意”,對(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
 
這里出什么問(wèn)題了?
上面的問(wèn)題之所以會(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)開(kāi)發(fā)者使用 lists 時(shí),這個(gè)問(wèn)題就更加常見(jiàn). 請(qǐng)看下面這個(gè)例子:
- >>> lst = [1, 2, 3]
 - >>> def foo1():
 - ... lst.append(5) # 沒(méi)有問(wèn)題...
 - ...
 - >>> foo1()
 - >>> lst
 - [1, 2, 3, 5]
 - >>> lst = [1, 2, 3]
 - >>> def foo2():
 - ... lst += [5] # ... 但是這里有問(wèn)題!
 - ...
 - >>> 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沒(méi)有問(wèn)題呢?
原因和之前那個(gè)例子的一樣,不過(guò)更加令人難以捉摸。foo1 沒(méi)有對(duì) lst 進(jìn)行賦值操作,而 foo2 做了。要知道, lst += [5] 是 lst = lst + [5] 的縮寫(xiě),我們?cè)噲D對(duì) lst 進(jìn)行賦值操作(Python把他當(dāng)成了局部變量)。此外,我們對(duì) lst 進(jìn)行的賦值操作是基于 lst 自身(這再一次被Python當(dāng)成了局部變量),但此時(shí)還未定義。因此出錯(cuò)!
常見(jiàn)錯(cuò)誤#5:當(dāng)?shù)鷷r(shí)修改一個(gè)列表(List)
下面代碼中的問(wèn)題應(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)的開(kāi)發(fā)者來(lái)說(shuō),這是一個(gè)眾所周知的錯(cuò)誤。盡管上面的例子非常明顯,但是許多高級(jí)開(kāi)發(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è)問(wèn)題是特別有用的,通過(guò)更改上文中的實(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]
 
常見(jiàn)錯(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ù)來(lái)生成一個(gè)匿名的函數(shù)以便實(shí)現(xiàn)我們想要的結(jié)果。有人說(shuō)這個(gè)方法很巧妙,有人說(shuō)它難以理解,還有人討厭這種做法。但是,如果你是一個(gè) Python 開(kāi)發(fā)者,理解這種行為很重要。
#p#
常見(jiàn)錯(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ì)出問(wèn)題的,不是嗎?
答案就是在Python中,僅僅引入一個(gè)循環(huán)依賴的模塊是沒(méi)有問(wèn)題的。如果一個(gè)模塊已經(jīng)被引入了,Python并不會(huì)去再次引入它。但是,根據(jù)每個(gè)模塊要訪問(wèn)其他模塊中的函數(shù)和變量位置的不同,就很可能會(huì)遇到問(wèn)題。
所以,回到我們這個(gè)例子,當(dāng)我們引入 a.py 時(shí),再引入 b.py 不會(huì)產(chǎn)生任何問(wè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 中都沒(méi)有調(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'
 
啊哦。 出問(wèn)題了!此處的問(wèn)題是,在引入b.py的過(guò)程中,Python嘗試去引入 a.py,但是a.py 要調(diào)用f(),而f() 有嘗試去訪問(wèn) b.x。但是此時(shí) b.x 還沒(méi)有被定義呢。所以發(fā)生了 AttributeError 異常。
至少,解決這個(gè)問(wèn)題很簡(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è)僖隻,沒(méi)有任何問(wèn)題:
- >>> 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'
 
常見(jiàn)錯(cuò)誤 #8: 與Python標(biāo)準(zhǔn)庫(kù)中的模塊命名沖突
 
Python一個(gè)令人稱贊的地方是它有豐富的模塊可供我們“開(kāi)箱即用”。但是,如果你沒(méi)有有意識(shí)的注意的話,就很容易出現(xiàn)你寫(xiě)的模塊和Python自帶的標(biāo)準(zhǔn)庫(kù)的模塊之間發(fā)生命名沖突的問(wèn)題(如,你也許有一個(gè)叫 email.py 的模塊,但這會(huì)和標(biāo)準(zhǔn)庫(kù)中的同名模塊沖突)。
這可能會(huì)導(dǎo)致很怪的問(wèn)題,例如,你引入了另一個(gè)模塊,但這個(gè)模塊要引入一個(gè)Python標(biāo)準(zhǔn)庫(kù)中的模塊,由于你定義了一個(gè)同名的模塊,就會(huì)使該模塊錯(cuò)誤的引入了你的模塊,而不是 stdlib 中的模塊。這就會(huì)出問(wèn)題了。
因此,我們必須要注意這個(gè)問(wèn)題,以避免使用和Python標(biāo)準(zhǔn)庫(kù)中相同的模塊名。修改你包中的模塊名要比通過(guò) Python Enhancement Proposal (PEP) 給Python提建議來(lái)修改標(biāo)準(zhǔn)庫(kù)的模塊名容易多了。
常見(jiàn)錯(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
 
出什么問(wèn)題了? “問(wèn)題”就是,在 Python 3 中,異常的對(duì)象在 except 代碼塊之外是不可見(jiàn)的。(這樣做的原因是,它將保存一個(gè)對(duì)內(nèi)存中堆棧幀的引用周期,直到垃圾回收器運(yùn)行并且從內(nèi)存中清除掉引用。了解更多技術(shù)細(xì)節(jié)請(qǐng)參考這里) 。
一種解決辦法是在 except 代碼塊的外部作用域中定義一個(gè)對(duì)異常對(duì)象的引用,以便訪問(wèn)。下面的例子使用了該方法,因此***的代碼可以在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í)的其他一些需要知道的重要差異。)
常見(jiàn)錯(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" >這里所說(shuō),當(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è)整潔并且可信賴的方法用來(lái)在程序退出之前做一些清理工作。很顯然,它是由foo.cleanup 來(lái)決定對(duì)綁定在 self.myhandle 上對(duì)象做些什么處理工作的,但是這就是你想要的。
總結(jié)
Python是一門(mén)強(qiáng)大的并且很靈活的語(yǔ)言,它有很多機(jī)制和語(yǔ)言規(guī)范來(lái)顯著的提高你的生產(chǎn)力。和其他任何一門(mén)語(yǔ)言或軟件一樣,如果對(duì)它能力的了解有限,這很可能會(huì)給你帶來(lái)阻礙,而不是好處。正如一句諺語(yǔ)所說(shuō)的那樣 “knowing enough to be dangerous”(譯者注:意思是自以為已經(jīng)了解足夠了,可以做某事了,但其實(shí)不是)。
熟悉Python的一些關(guān)鍵的細(xì)微之處,像本文中所提到的那些(但不限于這些),可以幫助我們更好的去使用語(yǔ)言,從而避免一些常見(jiàn)的陷阱。
你可以查看“Python 面試官指南” 來(lái)獲得一些關(guān)于如何辨別一個(gè)開(kāi)發(fā)者是否是Python專家的建議。
我們希望你在這篇文章中找到了一些對(duì)你有幫助的東西,并希望你得到你的反饋。
英文原文:Top 10 Mistakes that Python Programmers Make
譯文鏈接:http://www.oschina.net/translate/top-10-mistakes-that-python-programmers-make















 
 
 

 
 
 
 