Python是一門解釋性的,面向?qū)ο蟮模⒕哂袆討B(tài)語義的高級編程語言。它高級的內(nèi)置數(shù)據(jù)結(jié)構(gòu),結(jié)合其動態(tài)類型和動態(tài)綁定的特性,使得它在快速應(yīng)用程序開發(fā)(Rapid application Development)中頗為受歡迎,同時Python還能作為腳本語言或者膠水語言講現(xiàn)成的組件或者服務(wù)結(jié)合起來。Python支持模塊(modules)和包(packages),所以也鼓勵程序的模塊化以及代碼重用。
Python簡單、易學(xué)的語法可能會誤導(dǎo)一些Python程序員(特別是那些剛接觸這門語言的人們),可能會忽略某些細(xì)微之處和這門語言的強(qiáng)大之處。
考慮到這點,本文列出了“十大”甚至是高級的Python程序員都可能犯的,卻又不容易發(fā)現(xiàn)的細(xì)微錯誤。(注意:本文是針對比《Python程序員常見錯誤》稍微高級一點讀者,對于更加新手一點的Python程序員,有興趣可以讀一讀那篇文章)
Python允許給一個函數(shù)的某個參數(shù)設(shè)置默認(rèn)值以使該參數(shù)成為一個可選參數(shù)。盡管這是這門語言很棒的一個功能,但是這當(dāng)這個默認(rèn)值是可變對象(mutable)時,那就有些麻煩了。例如,看下面這個Python函數(shù)定義:
Python123 | >>> def foo(bar=[]): # bar是可選參數(shù),如果沒有指明的話,默認(rèn)值是[]... bar.append("baz") # 但是這行可是有問題的,走著瞧…... return bar |
人們常犯的一個錯誤是認(rèn)為每次調(diào)用這個函數(shù)時不給這個可選參數(shù)賦值的話,它總是會被賦予這個默認(rèn)表達(dá)式的值。例如,在上面的代碼中,程序員可能會認(rèn)為重復(fù)調(diào)用函數(shù)foo() (不傳參數(shù)bar給這個函數(shù)),這個函數(shù)會總是返回‘baz’,因為我們假定認(rèn)為每次調(diào)用foo()的時候(不傳bar),參數(shù)bar會被置為[](即,一個空的列表)。
那么我們來看看這么做的時候究竟會發(fā)生什么:
Python123456 | >>>foo()["baz"]>>>foo()["baz","baz"]>>>foo()["baz","baz","baz"] |
嗯?為什么每次調(diào)用foo()的時候,這個函數(shù)總是在一個已經(jīng)存在的列表后面添加我們的默認(rèn)值“baz”,而不是每次都創(chuàng)建一個新的列表?
答案是一個函數(shù)參數(shù)的默認(rèn)值,僅僅在該函數(shù)定義的時候,被賦值一次。如此,只有當(dāng)函數(shù)foo()第一次被定義的時候,才講參數(shù)bar的默認(rèn)值初始化到它的默認(rèn)值(即一個空的列表)。當(dāng)調(diào)用foo()的時候(不給參數(shù)bar),會繼續(xù)使用bar最早初始化時的那個列表。
由此,可以有如下的解決辦法:
Python123456789101112 | >>> def foo(bar=None):... if bar is None: # 或者用 if not bar:... bar = []... bar.append("baz")... return bar...>>> foo()["baz"]>>> foo()["baz"]>>> foo()["baz"] |
看下面一個例子:
Python1234567891011 | >>>classA(object):... x=1...>>>classB(A):... pass...>>>classC(A):... pass...>>>PRintA.x,B.x,C.x111 |
看起來沒有問題。
Python123 | >>> B.x = 2>>> print A.x, B.x, C.x1 2 1 |
嗯哈,還是和預(yù)想的一樣。
Python123 | >>>A.x=3>>>printA.x,B.x,C.x323 |
我了個去。只是改變了A.x,為啥C.x也變了?
在Python里,類變量通常在內(nèi)部被當(dāng)做字典來處理并遵循通常所說的方法解析順序(Method Resolution Order (MRO))。因此在上面的代碼中,因為屬性x在類C中找不到,因此它會往上去它的基類中查找(在上面的例子中只有A這個類,當(dāng)然Python是支持多重繼承(multiple inheritance)的)。換句話說,C沒有它自己獨立于A的屬性x。因此對C.x的引用實際上是對A.x的引用。(B.x不是對A.x的引用是因為在第二步里B.x=2將B.x引用到了2這個對象上,倘若沒有如此,B.x仍然是引用到A.x上的。——譯者注)
假設(shè)你有如下的代碼:
Python123456789 | >>> 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是用來將異常和這個可選的參數(shù)綁定起來(即這里的e),以用來在后面查看的。因此,在上面的代碼中,IndexError異常不會被except語句捕捉到;而最終ValueError這個異常被綁定在了一個叫做IndexError的參數(shù)上。
在except語句中捕捉多個異常的正確做法是將所有想要捕捉的異常放在一個元組(tuple)里并作為第一個參數(shù)給except語句。并且,為移植性考慮,使用as關(guān)鍵字,因為Python 2和Python 3都支持這樣的語法,例如:
Python1234567 | >>>try:... l=["a","b"]... int(l[2])...except(ValueError,IndexError)ase: ... pass...>>> |
Python的作用域解析是基于叫做LEGB(Local(本地),Enclosing(封閉),Global(全局),Built-in(內(nèi)置))的規(guī)則進(jìn)行操作的。這看起來很直觀,對吧?事實上,在Python中這有一些細(xì)微的地方很容易出錯。看這個例子:
Python12345678910 | >>> 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 fooUnboundLocalError: local variable 'x' referenced before assignment |
這是怎么回事?
這是因為,在一個作用域里面給一個變量賦值的時候,Python自動認(rèn)為這個變量是這個作用域的本地變量,并屏蔽作用域外的同名的變量。
很多時候可能在一個函數(shù)里添加一個賦值的語句會讓你從前本來工作的代碼得到一個UnboundLocalError。(感興趣的話可以讀一讀這篇文章。)
在使用列表(lists)的時候,這種情況尤為突出。看下面這個例子:
Python1234567891011121314151617 | >>>lst=[1,2,3]>>>deffoo1():... lst.append(5) # 這沒有問題......>>>foo1()>>>lst[1,2,3,5] >>>lst=[1,2,3]>>>deffoo2():... lst+=[5] # ... 這就有問題了!...>>>foo2()Traceback(mostrecentcalllast): File"<stdin>",line1,in<module> File"<stdin>",line2,infooUnboundLocalError:localvariable'lst'referencedbeforeassignment |
嗯?為什么foo2有問題,而foo1沒有問題?
答案和上一個例子一樣,但是更加不易察覺。foo1并沒有給lst賦值,但是foo2嘗試給lst賦值。注意lst+=[5]只是lst=lst+[5]的簡寫,由此可以看到我們嘗試給lst賦值(因此Python假設(shè)作用域為本地)。但是,這個要賦給lst的值是基于lst本身的(這里的作用域仍然是本地),而lst卻沒有被定義,這就出錯了。
下面這個例子中的代碼應(yīng)該比較明顯了:
Python123456789 | >>> 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 |
遍歷一個列表或者數(shù)組的同時又刪除里面的元素,對任何有經(jīng)驗的軟件開發(fā)人員來說這是個很明顯的錯誤。但是像上面的例子那樣明顯的錯誤,即使有經(jīng)驗的程序員也可能不經(jīng)意間在更加復(fù)雜的程序中不小心犯錯。
所幸,Python集成了一些優(yōu)雅的編程范式,如果使用得當(dāng),可以寫出相當(dāng)簡化和精簡的代碼。一個附加的好處是更簡單的代碼更不容易遇到這種“不小心在遍歷列表時刪掉列表元素”的bug。例如列表推導(dǎo)式(list comprehensions)就提供了這樣的范式。再者,列表推導(dǎo)式在避免這樣的問題上特別有用,接下來這個對上面的代碼的重新實現(xiàn)就相當(dāng)完美:
Python12345 | >>>odd=lambdax:bool(x%2)>>>numbers=[nforninrange(10)]>>>numbers[:]=[nforninnumbersifnotodd(n)] # 啊,這多優(yōu)美>>>numbers[0,2,4,6,8] |
看這個例子:
Python12345 | >>> def create_multipliers():... return [lambda x : i * x for i in range(5)]>>> for multiplier in create_multipliers():... print multiplier(2)... |
期望得到下面的輸出:
Python12345 | 02468 |
但是實際上得到的是:
Python12345 | 88888 |
意外吧!
這是由于Python的后期綁定(late binding)機(jī)制導(dǎo)致的,這是指在閉包中使用的變量的值,是在內(nèi)層函數(shù)被調(diào)用的時候查找的。因此在上面的代碼中,當(dāng)任一返回函數(shù)被調(diào)用的時候,i的值是在它被調(diào)用時的周圍作用域中查找(到那時,循環(huán)已經(jīng)結(jié)束了,所以i已經(jīng)被賦予了它最終的值4)。
解決的辦法比較巧妙:
Python1234567891011 | >>>defcreate_multipliers():... return[lambdax,i=i:i*xforiinrange(5)]...>>>formultiplierincreate_multipliers():... printmultiplier(2)...02468 |
這下對了!這里利用了默認(rèn)參數(shù)去產(chǎn)生匿名函數(shù)以達(dá)到期望的效果。有人會說這很優(yōu)美,有人會說這很微妙,也有人會覺得反感。但是如果你是一名Python程序員,重要的是能理解任何的情況。
假設(shè)你有兩個文件,a.py和b.py,在這兩個文件中互相加載對方,例如:
在a.py中:
Python1234 | import bdef f(): return b.xprint f() |
在b.py中:
Python1234 | importax=1defg(): printa.f() |
首先,我們試著加載a.py:
Python12 | >>> import a1 |
沒有問題。也許讓人吃驚,畢竟有個感覺應(yīng)該是問題的循環(huán)加載在這兒。
事實上在Python中僅僅是表面上的出現(xiàn)循環(huán)加載并不是什么問題。如果一個模塊以及被加載了,Python不會傻到再去重新加載一遍。但是,當(dāng)每個模塊都想要互相訪問定義在對方里的函數(shù)或者變量時,問題就來了。
讓我們再回到之前的例子,當(dāng)我們加載a.py時,它再加載b.py不會有問題,因為在加載b.py時,它并不需要訪問a.py的任何東西,而在b.py中唯一的引用就是調(diào)用a.f()。但是這個調(diào)用是在函數(shù)g()中完成的,并且a.py或者b.py中沒有人調(diào)用g(),所以這會兒心情還是美麗的。
但是當(dāng)我們試圖加載b.py時(之前沒有加載a.py),會發(fā)生什么呢:
Python12345678910 | >>>importbTraceback(mostrecentcalllast): File"<stdin>",line1,in<module> File"b.py",line1,in<module> importa File"a.py",line6,in<module>printf() File"a.py",line4,infreturnb.xAttributeError:'module'objecthasnoattribute'x' |
恭喜你,出錯了。這里問題出在加載b.py的過程中,Python試圖加載a.py,并且在a.py中需要調(diào)用到f(),而函數(shù)f()又要訪問到b.x,但是這個時候b.x卻還沒有被定義。這就產(chǎn)生了AttributeError異常。
解決的方案可以做一點細(xì)微的改動。改一下b.py,使得它在g()里面加載a.py:
Python1234 | x = 1def g(): import a # 只有當(dāng)g()被調(diào)用的時候才加載 print a.f() |
這會兒當(dāng)我們加載b.py的時候,一切安好:
Python1234 | >>>importb>>>b.g()1# 第一次輸出,因為模塊a在最后調(diào)用了‘print f()’1# 第二次輸出,這是我們調(diào)用g() |
Python的一個優(yōu)秀的地方在于它提供了豐富的庫模塊。但是這樣的結(jié)果是,如果你不下意識的避免,很容易你會遇到你自己的模塊的名字與某個隨Python附帶的標(biāo)準(zhǔn)庫的名字沖突的情況(比如,你的代碼中可能有一個叫做email.py的模塊,它就會與標(biāo)準(zhǔn)庫中同名的模塊沖突)。
這會導(dǎo)致一些很粗糙的問題,例如當(dāng)你想加載某個庫,這個庫需要加載Python標(biāo)準(zhǔn)庫里的某個模塊,結(jié)果呢,因為你有一個與標(biāo)準(zhǔn)庫里的模塊同名的模塊,這個包錯誤的將你的模塊加載了進(jìn)去,而不是加載Python標(biāo)準(zhǔn)庫里的那個模塊。這樣一來就會有麻煩了。
所以在給模塊起名字的時候要小心了,得避免與Python標(biāo)準(zhǔn)庫中的模塊重名。相比起你提交一個“Python改進(jìn)建議(Python Enhancement Proposal (PEP))”去向上要求改一個標(biāo)準(zhǔn)庫里包的名字,并得到批準(zhǔn)來說,你把自己的那個模塊重新改個名字要簡單得多。
看下面這個文件foo.py:
Python12345678910111213141516171819 | 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里,運行起來沒有問題:
123456 | $pythonfoo.py1keyerror1$pythonfoo.py2valueerror2 |
但是如果拿到Python 3上面玩玩:
12345678 | $ python3 foo.py 1key errorTraceback (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會在內(nèi)存的堆棧里保持一個引用鏈直到Python的垃圾處理將這些引用從內(nèi)存中清除掉。更多的技術(shù)細(xì)節(jié)可以參考這里。)
避免這樣的問題可以這樣做:保持在execpt塊作用域以外對異常對象的引用,這樣是可以訪問的。下面是用這個辦法對之前的例子做的改動,這樣在Python 2和Python 3里面都運行都沒有問題。
Python123456789101112131415161718192021 | importsys defbar(i): ifi==1: raiseKeyError(1) ifi==2: raiseValueError(2) defgood(): exception=None try: bar(int(sys.argv[1])) exceptKeyErrorase: exception=e print('key error') exceptValueErrorase: exception=e print('value error') print(exception) good() |
在Py3k里面運行:
123456 | $ python3 foo.py 1key error1$ python3 foo.py 2value error2 |
耶!
(順帶提一下,我們的“Python招聘指南”里討論了從Python 2移植代碼到Python 3時需要注意的其他重要的不同之處。)
假設(shè)有一個文件mod.py中這樣使用:
Python123456 | importfoo classBar(object): ... def__del__(self): foo.cleanup(self.myhandle) |
然后試圖在another_mod.py里這樣:
Python12 | import modmybar = mod.Bar() |
那么你會得到一個惡心的AttributeError異常。
為啥呢?這是因為(參考這里),當(dāng)解釋器關(guān)閉時,模塊所有的全局變量會被置為空(None)。結(jié)果便如上例所示,當(dāng)__del__被調(diào)用時,名字foo已經(jīng)被置為空了。
使用atexit.register()可以解決這個問題。如此,當(dāng)你的程序結(jié)束的時候(退出的時候),你的注冊的處理程序會在解釋器關(guān)閉之前處理。
這樣理解的話,對上面的mod.py可以做如下的修改:
Python12345678910 | importfooimportatexit defcleanup(handle): foo.cleanup(handle) classBar(object): def__init__(self): ... atexit.register(cleanup,self.myhandle) |
這樣的實現(xiàn)方式為在程序正常終止時調(diào)用清除功能提供了一種干凈可靠的辦法。顯然,需要foo.cleanup決定怎么處理綁定在self.myhandle上的對象,但你知道怎么做的。
Python 是一門非常強(qiáng)大且靈活的語言,它眾多的機(jī)制和范式能顯著的提高生產(chǎn)效率。不過,和任何一款軟件或者語言一樣,對它的理解或認(rèn)識不足的話,常常是弊大于利的,并會處于一種“一知半解”的狀態(tài)。
多熟悉Python的一些關(guān)鍵的細(xì)微的地方,比如(但不局限于)本文中提到的這些問題,可以幫你更好的使用這門語言的同時幫你避免一些常見的陷阱。
感興趣的話可以讀一讀這篇“Python面試指南(Insider’s Guide to Python Interviewing)”,了解一些能夠區(qū)分Python程序員的面試題目。
新聞熱點
疑難解答