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程序員的面試題目。
但願您能在本文學到有用的地方,並歡迎您的反饋。