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 ba
一個常見的錯誤是認為在函數每次不提供可選參數調用時可選參數将設定為預設指定值。在上面的代碼中,例如,人們可能會希望反複(即不明确指定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
這裡的問題在于 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 “”, line 2, in
IndexError: list index out of range
當疊代的時候,從一個 清單 (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)
...
你也許希望獲得下面的輸出結果:
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)
...
2
4
6
8
在這裡,我們利用了預設參數來生成一個匿名的函數以便實作我們想要的結果。有人說這個方法很巧妙,有人說它難以了解,還有人讨厭這種做法。但是,如果你是一個 Python 開發者,了解這種行為很重要。
你定義一個函數,函數内的變量并不是立刻就把值綁定了,而是等調用的時候再查找這個變量,如圖,定義函數的時候沒有 foo 變量,但是仍然可以,隻要調用的時候環境裡有就行。
一個道理,在 for 裡面 i 的值是不斷改寫的,但是 lambda 裡面隻是儲存了 i 的符号,調用的時候再查找。這就是你說的後期綁定。
為什麼你加了預設參數就成功了呢?因為在建立函數的時候就要擷取預設參數的值,放到 lambda 的環境中,是以這裡相當于存在一個指派,進而 lambda 函數環境中有了一個獨立的 i。
最後,優雅的寫法是用生成器:
for multiplier in (lambda x : i * x for i in range(5)):
print(multiplier(2))
這樣惰性求值就可以避免 i 的改寫。
或者:
def create_multipliers():
for i in range(5):
yield lambda x: i * x
for multiplier in create_multipliers():
print(multiplier(2))
常見錯誤 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'
啊哦。 出問題了!此處的問題是,在引入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标準庫中的子產品命名沖突
常見錯誤 ``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
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__方法
假設你有一個名為 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的一些關鍵的細微之處,像本文中所提到的那些(但不限于這些),可以幫助我們更好的去使用語言,進而避免一些常見的陷阱。
問題一:以下的代碼的輸出将是什麼? 說出你的答案并解釋。
class Parent(object):
x = 1
class Child1(Parent):
pass
class Child2(Parent):
pass
print Parent.x, Child1.x, Child2.x
Child1.x = 2
print Parent.x, Child1.x, Child2.x
Parent.x = 3
print Parent.x, Child1.x, Child2.x
答案
以上代碼的輸出是:
1
1
1
1
2
1
3
2
3
使你困惑或是驚奇的是關于最後一行的輸出是 3 2 3 而不是 3 2 1。為什麼改變了 Parent.x的值還會改變 Child2.x 的值,但是同時 Child1.x 值卻沒有改變?
這個答案的關鍵是,在 Python 中,類變量在内部是作為字典處理的。如果一個變量的名字沒有在目前類的字典中發現,将搜尋祖先類(比如父類)直到被引用的變量名被找到(如果這個被引用的變量名既沒有在自己所在的類又沒有在祖先類中找到,會引發一個 AttributeError 異常 )。
是以,在父類中設定 x = 1 會使得類變量 X 在引用該類和其任何子類中的值為 1。這就是因為第一個 print 語句的輸出是 1 1 1。
随後,如果任何它的子類重寫了該值(例如,我們執行語句 Child1.x = 2),然後,該值僅僅在子類中被改變。這就是為什麼第二個 print 語句的輸出是 1 2 1。
最後,如果該值在父類中被改變(例如,我們執行語句 Parent.x = 3),這個改變會影響到任何未重寫該值的子類當中的值(在這個示例中被影響的子類是 Child2)。這就是為什麼第三個 print 輸出是 3 2 3。
問題二:以下的代碼的輸出将是什麼? 說出你的答案并解釋?
def div1(x,y):
print("%s/%s = %s" % (x, y, x/y))
def div2(x,y):
print("%s//%s = %s" % (x, y, x//y))
div1(5,2)
div1(5.,2)
div2(5,2)
div2(5.,2.)
答案
這個答案實際依賴于你使用的是 Python 2 還是 Python 3。
在 Python 3 中,期望的輸出是:
5/2
=
2.5
5.0/2
=
2.5
5//2
= 2
5.0//2.0
= 2.0
在 Python 2 中,盡管如此,以上代碼的輸出将是:
5/2 = 2
5.0/2 = 2.5
5//2 = 2
5.0//2.0 = 2.0
預設,如果兩個操作數都是整數,Python 2 自動執行整型計算。結果,
5/2
值為
2
,然而
5./2
值為 ```2.5``。注意,盡管如此,你可以在 Python 2 中重載這一行為(比如達到你想在 Python 3 中的同樣結果),通過添加以下導入:
from __future__ import division
也需要注意的是“雙劃線”(//)操作符将一直執行整除,而不管操作數的類型,這就是為什麼
5.0//2.0
值為
2.0
。注: 在 Python 3 中,/ 操作符是做浮點除法,而 // 是做整除(即商沒有餘數,比如 10 // 3 其結果就為 3,餘數會被截除掉,而 (-7) // 3 的結果卻是 -3。這個算法與其它很多程式設計語言不一樣,需要注意,它們的整除運算會向0的方向取值。而在 Python 2 中,/ 就是整除,即和 Python 3 中的 // 操作符一樣,)
問題三:以下代碼将輸出什麼?
list = ['a', 'b', 'c', 'd', 'e']
print list[10:]
答案
以上代碼将輸出 [],并且不會導緻一個 IndexError。
正如人們所期望的,試圖通路一個超過清單索引值的成員将導緻 IndexError(比如通路以上清單的 list[10])。盡管如此,試圖通路一個清單的以超出清單成員數作為開始索引的切片将不會導緻 IndexError,并且将僅僅傳回一個空清單。
一個讨厭的小問題是它會導緻出現 bug ,并且這個問題是難以追蹤的,因為它在運作時不會引發錯誤。
問題四:以下的代碼的輸出将是什麼? 說出你的答案并解釋?
def
multipliers():
return[lambdax:i *xfor i in range(4)]
print[m(2) for m in multipliers()]
你将如何修改 multipliers 的定義來産生期望的結果
答案
以上代碼的輸出是 [6, 6, 6, 6] (而不是 [0, 2, 4, 6])。
這個的原因是 Python 的閉包的後期綁定導緻的 late binding,這意味着在閉包中的變量是在内部函數被調用的時候被查找。是以結果是,當任何 multipliers() 傳回的函數被調用,在那時,i 的值是在它被調用時的周圍作用域中查找,到那時,無論哪個傳回的函數被調用,for 循環都已經完成了,i 最後的值是 3,是以,每個傳回的函數 multiplies 的值都是 3。是以一個等于 2 的值被傳遞進以上代碼,它們将傳回一個值 6 (比如: 3 x 2)。
(順便說下,正如在 The Hitchhiker’s Guide to Python 中指出的,這裡有一點普遍的誤解,是關于 lambda 表達式的一些東西。一個 lambda 表達式建立的函數不是特殊的,和使用一個普通的 def 建立的函數展示的表現是一樣的。)
這裡有兩種方法解決這個問題。
最普遍的解決方案是建立一個閉包,通過使用預設參數立即綁定它的參數。例如:
def multipliers():
return [lambda x, i=i : i * x for i in range(4)]
另外一個選擇是,你可以使用
functools.partial
函數:
from functools import partial
from operator import mul
def
multipliers():
return [partial(mul,i) for i in range(4)]
問題五:以下的代碼的輸出将是什麼? 說出你的答案并解釋?
def extendList(val, list=[]):
list.append(val)
return list
list1 = extendList(10)
list2 = extendList(123,[])
list3 = extendList('a')
print "list1 = %s" % list1
print "list2 = %s" % list2
print "list3 = %s" % list3
你将如何修改
extendList
的定義來産生期望的結果
以上代碼的輸出為:
list1 =[10,'a']
list2=[123]
list3=[10,'a']
許多人會錯誤的認為 list1 應該等于 [10] 以及 list3 應該等于 [‘a’]。認為 list 的參數會在 extendList 每次被調用的時候會被設定成它的預設值 []。
盡管如此,實際發生的事情是,新的預設清單僅僅隻在函數被定義時建立一次。随後當 extendList 沒有被指定的清單參數調用的時候,其使用的是同一個清單。這就是為什麼當函數被定義的時候,表達式是用預設參數被計算,而不是它被調用的時候。
是以,list1 和 list3 是操作的相同的清單。而 list2是操作的它建立的獨立的清單(通過傳遞它自己的空清單作為list 參數的值)。
extendList 函數的定義可以做如下修改,但,當沒有新的 list 參數被指定的時候,會總是開始一個新清單,這更加可能是一直期望的行為。
def extendList(val, list=None):
if list is None:
list = []
list.append(val)
return list
使用這個改進的實作,輸出将是:
list1=[10]
list2=[123]
list3=['a']