常見(jiàn)錯(cuò)誤1:錯(cuò)誤地將表達(dá)式作為函數(shù)的默認(rèn)參數(shù)
在Python中姆坚,我們可以為函數(shù)的某個(gè)參數(shù)設(shè)置默認(rèn)值译断,使該參數(shù)成為可選參數(shù)萧恕。雖然這是一個(gè)很好的語(yǔ)言特性漾狼,但是當(dāng)默認(rèn)值是可變類(lèi)型時(shí)鸠蚪,也會(huì)導(dǎo)致一些令人困惑的情況今阳。我們來(lái)看看下面這個(gè)Python函數(shù)定義:
>>> def foo(bar=[]):? ? ? ? # bar是可選參數(shù),如果沒(méi)有提供bar的值茅信,則默認(rèn)為[]盾舌,
...? ? bar.append("baz")? ? # 但是稍后我們會(huì)看到這行代碼會(huì)出現(xiàn)問(wèn)題。
...? ? return bar
Python程序員常犯的一個(gè)錯(cuò)誤蘸鲸,就是想當(dāng)然地認(rèn)為:在每次調(diào)用函數(shù)時(shí)妖谴,如果沒(méi)有為可選參數(shù)傳入值,那么這個(gè)可選參數(shù)就會(huì)被設(shè)置為指定的默認(rèn)值酌摇。在上面的代碼中窖维,你們可能覺(jué)得重復(fù)調(diào)用foo()函數(shù)應(yīng)該會(huì)一直返回’baz’,因?yàn)槟銈兡J(rèn)每次foo()函數(shù)執(zhí)行時(shí)(沒(méi)有指定bar變量的值)妙痹,bar變量都被設(shè)置為[](也就是,一個(gè)新的空列表)鼻疮。
?
加群923414804免費(fèi)獲取數(shù)十套PDF資料
但是怯伊,實(shí)際運(yùn)行結(jié)果卻是這樣的:
>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]
很奇怪吧?為什么每次調(diào)用foo()函數(shù)時(shí)判沟,都會(huì)把”baz”這個(gè)默認(rèn)值添加到已有的列表中耿芹,而不是重新創(chuàng)建一個(gè)新的空列表呢?
答案就是挪哄,可選參數(shù)默認(rèn)值的設(shè)置在Python中只會(huì)被執(zhí)行一次吧秕,也就是定義該函數(shù)的時(shí)候。因此迹炼,只有當(dāng)foo()函數(shù)被定義時(shí)砸彬,bar參數(shù)才會(huì)被初始化為默認(rèn)值(也就是,一個(gè)空列表)斯入,但是之后每次foo()函數(shù)被調(diào)用時(shí)砂碉,都會(huì)繼續(xù)使用bar參數(shù)原先初始化生成的那個(gè)列表。
當(dāng)然刻两,一個(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)問(wèn)題2:錯(cuò)誤地使用類(lèi)變量
我們來(lái)看下面這個(gè)例子:
>>> class A(object):
...? ? x = 1
...
>>> class B(A):
...? ? pass
...
>>> class C(A):
...? ? pass
...
>>> print A.x, B.x, C.x
1 1 1
這個(gè)結(jié)果很正常增蹭。
>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1
嗯,結(jié)果和預(yù)計(jì)的一樣磅摹。
>>> A.x = 3
>>> print A.x, B.x, C.x
3 2 3
在Python語(yǔ)言中滋迈,類(lèi)變量是以字典的形式進(jìn)行處理的霎奢,并且遵循方法解析順序(Method Resolution Order,MRO)饼灿。因此幕侠,在上面的代碼中,由于類(lèi)C中并沒(méi)有x這個(gè)屬性赔退,解釋器將會(huì)查找它的基類(lèi)(base class橙依,盡管Python支持多重繼承,但是在這個(gè)例子中硕旗,C的基類(lèi)只有A)窗骑。換句話說(shuō),C并不沒(méi)有獨(dú)立于A漆枚、真正屬于自己的x屬性创译。所以,引用C.x實(shí)際上就是引用了A.x墙基。如果沒(méi)有處理好這里的關(guān)系软族,就會(huì)導(dǎo)致示例中出現(xiàn)的這個(gè)問(wèn)題。
常見(jiàn)錯(cuò)誤3:錯(cuò)誤地指定異常代碼塊(exception block)的參數(shù)
請(qǐng)看下面這段代碼:
>>> 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中,需要使用變量e將異常綁定至可選的第二個(gè)參數(shù)中初茶,才能進(jìn)一步查看異常的情況颗祝。因此,在上述代碼中恼布,except語(yǔ)句并沒(méi)有捕獲IndexError異常螺戳;而是將出現(xiàn)的異常綁定到了一個(gè)名為IndexError的參數(shù)中。
要想在except語(yǔ)句中正確地捕獲多個(gè)異常折汞,則應(yīng)將第一個(gè)參數(shù)指定為元組倔幼,然后在元組中寫(xiě)下希望捕獲的異常類(lèi)型。另外爽待,為了提高可移植性损同,請(qǐng)使用as關(guān)鍵詞,Python 2和Python 3均支持這種用法堕伪。
>>> try:
...? ? l = ["a", "b"]
...? ? int(l[2])
... except (ValueError, IndexError) as e:?
...? ? pass
...
>>>
常見(jiàn)錯(cuò)誤4:錯(cuò)誤理解Python中的變量名解析
Python中的變量名解析遵循所謂的LEGB原則揖庄,也就是“L:本地作用域;E:上一層結(jié)構(gòu)中def或lambda的本地作用域欠雌;G:全局作用域蹄梢;B:內(nèi)置作用域”(Local,Enclosing,Global禁炒,Builtin)而咆,按順序查找∧桓ぃ看上去是不是很簡(jiǎn)單暴备?不過(guò),事實(shí)上這個(gè)原則的生效方式還是有著一些特殊之處们豌。說(shuō)到這點(diǎn)涯捻,我們就不得不提下面這個(gè)常見(jiàn)的Python編程錯(cuò)誤。請(qǐng)看下面的代碼:
>>> 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)題望迎?
上述錯(cuò)誤的出現(xiàn)障癌,是因?yàn)楫?dāng)你在某個(gè)作用域內(nèi)為變量賦值時(shí),該變量被Python解釋器自動(dòng)視作該作用域的本地變量辩尊,并會(huì)取代任何上一層作用域中相同名稱(chēng)的變量涛浙。
正是因?yàn)檫@樣,才會(huì)出現(xiàn)一開(kāi)始好好的代碼摄欲,在某個(gè)函數(shù)內(nèi)部添加了一個(gè)賦值語(yǔ)句之后卻出現(xiàn)了UnboundLocalError轿亮,難怪會(huì)讓許多人吃驚。
在使用列表時(shí)胸墙,Python程序員尤其容易陷入這個(gè)圈套我注。
請(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]? ? ? # ... 但這里就不對(duì)了!
...
>>> 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
呃?為什么函數(shù)foo1運(yùn)行正常迟隅,foo2卻出現(xiàn)了錯(cuò)誤仓手?
答案與上一個(gè)示例相同,但是卻更難捉摸清楚玻淑。foo1函數(shù)并沒(méi)有為lst變量進(jìn)行賦值,但是foo2卻有賦值呀伙。我們知道补履,lst += [5]只是lst = lst + [5]的簡(jiǎn)寫(xiě),從中我們就可以看出剿另,foo2函數(shù)在嘗試為lst賦值(因此箫锤,被Python解釋器認(rèn)為是函數(shù)本地作用域的變量)。但是雨女,我們希望為lst賦的值卻又是基于lst變量本身(這時(shí)谚攒,也被認(rèn)為是函數(shù)本地作用域內(nèi)的變量),也就是說(shuō)該變量還沒(méi)有被定義氛堕。這才出現(xiàn)了錯(cuò)誤馏臭。
常見(jiàn)錯(cuò)誤5:在遍歷列表時(shí)更改列表
下面這段代碼的問(wèn)題應(yī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
在遍歷列表或數(shù)組的同時(shí)從中刪除元素,是任何經(jīng)驗(yàn)豐富的Python開(kāi)發(fā)人員都會(huì)注意的問(wèn)題讼稚。但是盡管上面的示例十分明顯括儒,資深開(kāi)發(fā)人員在編寫(xiě)更為復(fù)雜代碼的時(shí)候绕沈,也很可能會(huì)無(wú)意之下犯同樣的錯(cuò)誤。
幸運(yùn)的是帮寻,Python語(yǔ)言融合了許多優(yōu)雅的編程范式乍狐,如果使用得當(dāng),可以極大地簡(jiǎn)化代碼固逗。簡(jiǎn)化代碼還有一個(gè)好處浅蚪,就是不容易出現(xiàn)在遍歷列表時(shí)刪除元素這個(gè)錯(cuò)誤。能夠做到這點(diǎn)的一個(gè)編程范式就是列表解析式烫罩。而且惜傲,列表解析式在避免這個(gè)問(wèn)題方面尤其有用,下面用列表解析式重新實(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在閉包中如何綁定變量
請(qǐng)看下面這段代碼:
>>> def create_multipliers():
...? ? return [lambda x : i * x for i in range(5)]
>>> for multiplier in create_multipliers():
...? ? print multiplier(2)
...
你可能覺(jué)得輸出結(jié)果應(yīng)該是這樣的:
0
2
4
6
8
但是嗡髓,實(shí)際的輸出結(jié)果卻是:
8
8
8
8
8
嚇了一跳吧操漠!
這個(gè)結(jié)果的出現(xiàn),主要是因?yàn)镻ython中的遲綁定(late binding)機(jī)制饿这,即閉包中變量的值只有在內(nèi)部函數(shù)被調(diào)用時(shí)才會(huì)進(jìn)行查詢浊伙。因此,在上面的代碼中长捧,每次create_multipliers()所返回的函數(shù)被調(diào)用時(shí)嚣鄙,都會(huì)在附近的作用域中查詢變量i的值(而到那時(shí),循環(huán)已經(jīng)結(jié)束串结,所以變量i最后被賦予的值為4)哑子。
要解決這個(gè)常見(jiàn)Python問(wè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
請(qǐng)注意肌割!我們?cè)谶@里利用了默認(rèn)參數(shù)來(lái)實(shí)現(xiàn)這個(gè)lambda匿名函數(shù)卧蜓。有人可能認(rèn)為這樣做很優(yōu)雅,有人會(huì)覺(jué)得很巧妙把敞,還有人會(huì)嗤之以鼻弥奸。但是,如果你是一名Python程序員奋早,不管怎樣你都應(yīng)該要了解這種解決方法盛霎。
常見(jiàn)錯(cuò)誤7:模塊之間出現(xiàn)循環(huán)依賴(circular dependencies)
假設(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()
首先,我們嘗試導(dǎo)入a.py模塊:
>>> import a
1
代碼運(yùn)行正常掉奄。也許這出乎了你的意料规个。畢竟,我們這里存在循環(huán)引用這個(gè)問(wèn)題,想必應(yīng)該是會(huì)出現(xiàn)問(wèn)題的绰姻,難道不是嗎枉侧?
答案是,僅僅存在循環(huán)引用的情況本身并不會(huì)導(dǎo)致問(wèn)題狂芋。如果一個(gè)模塊已經(jīng)被引用了榨馁,Python可以做到不再次進(jìn)行引用。但是如果每個(gè)模塊試圖訪問(wèn)其他模塊定義的函數(shù)或變量的時(shí)機(jī)不對(duì)帜矾,那么你就很可能陷入困境翼虫。
那么回到我們的示例,當(dāng)我們導(dǎo)入a.py模塊時(shí)屡萤,它在引用b.py模塊時(shí)是不會(huì)出現(xiàn)問(wèn)題的珍剑,因?yàn)閎.py模塊在被引用時(shí),并不需要訪問(wèn)在a.py模塊中定義的任何變量或函數(shù)死陆。b.py模塊中對(duì)a模塊唯一的引用招拙,就是調(diào)用了a模塊的foo()函數(shù)。但是那個(gè)函數(shù)調(diào)用發(fā)生在g()函數(shù)當(dāng)中措译,而a.py或b.py模塊中都沒(méi)有調(diào)用g()函數(shù)别凤。所以,不會(huì)出現(xiàn)問(wèn)題领虹。
但是规哪,如果我們?cè)囍鴮?dǎo)入b.py模塊呢(即之前沒(méi)有引用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)題是诉稍,在導(dǎo)入b.py的過(guò)程中,它試圖引用a.py模塊最疆,而a.py模塊接著又要調(diào)用foo()函數(shù)杯巨,這個(gè)foo()函數(shù)接著又試圖去訪問(wèn)b.x變量。但是這個(gè)時(shí)候努酸,b.x變量還沒(méi)有被定義舔箭,所以才出現(xiàn)了AttributeError異常。
解決這個(gè)問(wèn)題有一種非常簡(jiǎn)單的方法蚊逢,就是簡(jiǎn)單地修改下b.py模塊,在g()函數(shù)內(nèi)部才引用a.py:
x = 1
def g():
? ? import a? # This will be evaluated only when g() is called
? ? print a.f()
現(xiàn)在我們?cè)賹?dǎo)入b.py模塊的話箫章,就不會(huì)出現(xiàn)任何問(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語(yǔ)言的一大優(yōu)勢(shì)烙荷,就是其本身自帶的強(qiáng)大標(biāo)準(zhǔn)庫(kù)。但是檬寂,正因?yàn)槿绱酥粘椋绻悴蝗タ桃庾⒁獾脑挘阋彩怯锌赡転樽约旱哪K取一個(gè)和Python自帶標(biāo)準(zhǔn)庫(kù)模塊相同的名字(例如,如果你的代碼中有一個(gè)模塊叫email.py昼伴,那么這就會(huì)與Python標(biāo)準(zhǔn)庫(kù)中同名的模塊相沖突匾旭。)
這很可能會(huì)給你帶來(lái)難纏的問(wèn)題。舉個(gè)例子圃郊,在導(dǎo)入模塊A的時(shí)候价涝,假如該模塊A試圖引用Python標(biāo)準(zhǔn)庫(kù)中的模塊B,但卻因?yàn)槟阋呀?jīng)有了一個(gè)同名模塊B持舆,模塊A會(huì)錯(cuò)誤地引用你自己代碼中的模塊B色瘩,而不是Python標(biāo)準(zhǔn)庫(kù)中的模塊B。這也是導(dǎo)致一些嚴(yán)重錯(cuò)誤的原因逸寓。
因此居兆,Python程序員要格外注意,避免使用與Python標(biāo)準(zhǔn)庫(kù)模塊相同的名稱(chēng)竹伸。畢竟泥栖,修改自己模塊的名稱(chēng)比提出PEP提議修改上游模塊名稱(chēng)且讓提議通過(guò),要來(lái)得容易的多勋篓。
常見(jiàn)錯(cuò)誤9:未能解決Python 2與Python 3之間的差異
假設(shè)有下面這段代碼:
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)題”是耙蔑,在Python 3中,異常對(duì)象在except代碼塊作用域之外是無(wú)法訪問(wèn)的孤荣。(這么設(shè)計(jì)的原因在于甸陌,如果不這樣的話,堆棧幀中就會(huì)一直保留它的引用循環(huán)盐股,直到垃圾回收器運(yùn)行钱豁,將引用從內(nèi)存中清除。)
避免這個(gè)問(wèn)題的一種方法疯汁,就是在except代碼塊的作用域之外牲尺,維持一個(gè)對(duì)異常對(duì)象的引用(reference),這樣異常對(duì)象就可以訪問(wèn)了幌蚊。下面這段代碼就使用了這種方法谤碳,因此在Python 2和Python 3中的輸出結(jié)果是一致的:
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()
在Python 3下運(yùn)行代碼:
$ python3 foo.py 1
key error
1
$ python3 foo.py 2
value error
2
太棒了!
常見(jiàn)錯(cuò)誤10:錯(cuò)誤使用del方法
假設(shè)你在mod.py的文件中編寫(xiě)了下面的代碼:
import foo
class Bar(object):
? ? ? ? ...
? ? def __del__(self):
? ? ? ? foo.cleanup(self.myhandle)
之后溢豆,你在another_mod.py文件中進(jìn)行如下操作:
import mod
mybar = mod.Bar()
如果你運(yùn)行another_mod.py模塊的話蜒简,將會(huì)出現(xiàn)AttributeError異常。
為什么漩仙?因?yàn)楫?dāng)解釋器結(jié)束運(yùn)行的時(shí)候搓茬,該模塊的全局變量都會(huì)被設(shè)置為None犹赖。因此,在上述示例中卷仑,當(dāng)__del__方法被調(diào)用之前峻村,foo已經(jīng)被設(shè)置成了None。
要想解決這個(gè)有點(diǎn)棘手的Python編程問(wèn)題锡凝,其中一個(gè)辦法就是使用atexit.register()方法粘昨。這樣的話,當(dāng)你的程序執(zhí)行完成之后(即正常退出程序的情況下)私爷,你所指定的處理程序就會(huì)在解釋器關(guān)閉之前運(yùn)行雾棺。
應(yīng)用了上面這種方法,修改后的mod.py文件可能會(huì)是這樣子的:
import foo
import atexit
def cleanup(handle):
? ? foo.cleanup(handle)
class Bar(object):
? ? def __init__(self):
? ? ? ? ...
? ? ? ? atexit.register(cleanup, self.myhandle)
這種實(shí)現(xiàn)支持在程序正常終止時(shí)干凈利落地調(diào)用任何必要的清理功能衬浑。很明顯捌浩,上述示例中將會(huì)由foo.cleanup函數(shù)來(lái)決定如何處理self.myhandle所綁定的對(duì)象。
綜述
Python是一門(mén)強(qiáng)大而又靈活的編程語(yǔ)言工秩,提供的許多編程機(jī)制和范式可以極大地提高工作效率尸饺。但是與任何軟件工具或語(yǔ)言一樣,如果對(duì)該語(yǔ)言的能力理解有限或無(wú)法欣賞助币,那么有時(shí)候自己反而會(huì)被阻礙浪听,而不是受益了。正如一句諺語(yǔ)所說(shuō)眉菱,“自以為知道夠多迹栓,但實(shí)則會(huì)給自己或別人帶來(lái)危險(xiǎn)”(knowing enough to be dangerous)。(譯者注:這句諺語(yǔ)的意思是俭缓,自以為已經(jīng)對(duì)某件事情了解足夠克伊,但在實(shí)際去執(zhí)行或?qū)嵤r(shí),卻會(huì)給自己和別人帶來(lái)危險(xiǎn)华坦。)
不斷地熟悉Python語(yǔ)言的一些細(xì)微之處愿吹,尤其是本文中提到的10大常見(jiàn)錯(cuò)誤,將會(huì)幫助你有效地使用這門(mén)語(yǔ)言惜姐,同時(shí)也能避免犯一些比較常見(jiàn)的錯(cuò)誤犁跪。