十個Python程式員易犯的錯誤

來源:互聯網
上載者:User
不管是在學習還是工作過程中,人都會犯錯。雖然Python的文法簡單、靈活,但也一樣存在一些不小的坑,一不小心,初學者和資深Python程式員都有可能會栽跟頭。本文為大家分享了10大常見錯誤,需要的朋友可以參考下
常見錯誤1:錯誤地將運算式作為函數的預設參數

在Python中,我們可以為函數的某個參數設定預設值,使該參數成為選擇性參數。雖然這是一個很好的語言特性,但是當預設值是可變類型時,也會導致一些令人困惑的情況。我們來看看下面這個Python函數定義:
>>> def foo(bar=[]): # bar是選擇性參數,如果沒有提供bar的值,則預設為[],
... bar.append("baz") # 但是稍後我們會看到這行代碼會出現問題。
... return bar

Python程式員常犯的一個錯誤,就是想當然地認為:在每次調用函數時,如果沒有為選擇性參數傳入值,那麼這個選擇性參數就會被設定為指定的預設值。在上面的代碼中,你們可能覺得重複調用foo()函數應該會一直返回'baz',因為你們預設每次foo()函數執行時(沒有指定bar變數的值),bar變數都被設定為[](也就是,一個新的空列表)。

但是,實際運行結果卻是這樣的:
>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]

很奇怪吧?為什麼每次調用foo()函數時,都會把"baz"這個預設值添加到已有的列表中,而不是重新建立一個新的空列表呢?

答案就是,選擇性參數預設值的設定在Python中只會被執行一次,也就是定義該函數的時候。因此,只有當foo()函數被定義時,bar參數才會被初始化為預設值(也就是,一個空列表),但是之後每次foo()函數被調用時,都會繼續使用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

在Python語言中,類變數是以字典的形式進行處理的,並且遵循方法解析順序(Method Resolution Order,MRO)。因此,在上面的代碼中,由於類C中並沒有x這個屬性,解譯器將會尋找它的基類(base class,儘管Python支援多重繼承,但是在這個例子中,C的基類只有A)。換句話說,C並不沒有獨立於A、真正屬於自己的x屬性。所以,引用C.x實際上就是引用了A.x。如果沒有處理好這裡的關係,就會導致樣本中出現的這個問題。
常見錯誤3:錯誤地指定異常代碼塊(exception block)的參數

請看下面這段代碼:
>>> try:
... l = ["a", "b"]
... int(l[2])
... except ValueError, IndexError: # To catch both exceptions, right?
... pass
...
Traceback (most recent call last):
File "<stdin>"</stdin>, line 3, in <module>
IndexError: list index out of range

這段代碼的問題在於,except語句並不支援以這種方式指定異常。在Python 2.x中,需要使用變數e將異常綁定至可選的第二個參數中,才能進一步查看異常的情況。因此,在上述代碼中,except語句並沒有捕獲IndexError異常;而是將出現的異常綁定到了一個名為IndexError的參數中。

要想在except語句中正確地捕獲多個異常,則應將第一個參數指定為元組,然後在元組中寫下希望捕獲的異常類型。另外,為了提高可移植性,請使用as關鍵詞,Python 2和Python 3均支援這種用法。
>>> try:
... l = ["a", "b"]
... int(l[2])
... except (ValueError, IndexError) as e:
... pass
...
>>>
常見錯誤4:錯誤理解Python中的變數名解析

Python中的變數名解析遵循所謂的LEGB原則,也就是“L:本地範圍;E:上一層結構中def或lambda的本地範圍;G:全域範圍;B:內建範圍”(Local,Enclosing,Global,Builtin),按順序尋找。看上去是不是很簡單?不過,事實上這個原則的生效方式還是有著一些特殊之處。說到這點,我們就不得不提下面這個常見的Python編程錯誤。請看下面的代碼:
>>> x = 10
>>> def foo():
... x += 1
... print x
...
>>> foo()
Traceback (most recent call last):
File "<stdin>"</stdin>, line 1, in <module>
File "<stdin>"</stdin>, line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment

出了什麼問題?

上述錯誤的出現,是因為當你在某個範圍內為變數賦值時,該變數被Python解譯器自動視作該範圍的本地變數,並會取代任何上一層範圍中相同名稱的變數。

正是因為這樣,才會出現一開始好好的代碼,在某個函數內部添加了一個指派陳述式之後卻出現了UnboundLocalError,難怪會讓許多人吃驚。

在使用列表時,Python程式員尤其容易陷入這個圈套。

請看下面這個程式碼範例:
>>> 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>"</stdin>, line 1, in <module>
File "<stdin>"</stdin>, line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment

呃?為什麼函數foo1運行正常,foo2卻出現了錯誤?

答案與上一個樣本相同,但是卻更難捉摸清楚。foo1函數並沒有為lst變數進行賦值,但是foo2卻有賦值。我們知道,lst += [5]只是lst = lst + [5]的簡寫,從中我們就可以看出,foo2函數在嘗試為lst賦值(因此,被Python解譯器認為是函數本地範圍的變數)。但是,我們希望為lst賦的值卻又是基於lst變數本身(這時,也被認為是函數本地範圍內的變數),也就是說該變數還沒有被定義。這才出現了錯誤。
常見錯誤5:在遍曆列表時更改列表

下面這段代碼的問題應該算是十分明顯:
>>> 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>"</stdin>, line 2, in <module>
IndexError: list index out of range

在遍曆列表或數組的同時從中刪除元素,是任何經驗豐富的Python開發人員都會注意的問題。但是儘管上面的樣本十分明顯,資深開發人員在編寫更為複雜代碼的時候,也很可能會無意之下犯同樣的錯誤。

幸運的是,Python語言融合了許多優雅的編程範式,如果使用得當,可以極大地簡化代碼。簡化代碼還有一個好處,就是不容易出現在遍曆列表時刪除元素這個錯誤。能夠做到這點的一個編程範式就是列表解析式。而且,列表解析式在避免這個問題方面尤其有用,下面用列表解析式重新實現上面代碼的功能:
>>> 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)
...

你可能覺得輸出結果應該是這樣的:

但是,實際的輸出結果卻是:

嚇了一跳吧!

這個結果的出現,主要是因為Python中的遲綁定(late binding )機制,即閉包中變數的值只有在內建函式被調用時才會進行查詢。因此,在上面的代碼中,每次create_multipliers()所返回的函數被調用時,都會在附近的範圍中查詢變數i的值(而到那時,迴圈已經結束,所以變數i最後被賦予的值為4)。

要解決這個常見Python問題的方法中,需要使用一些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

請注意!我們在這裡利用了預設參數來實現這個lambda匿名函數。有人可能認為這樣做很優雅,有人會覺得很巧妙,還有人會嗤之以鼻。但是,如果你是一名Python程式員,不管怎樣你都應該要瞭解這種解決方案。
常見錯誤7:模組之間出現循環相依性(circular dependencies)

假設你有兩個檔案,分別是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模組:

代碼運行正常。也許這出乎了你的意料。畢竟,我們這裡存在循環參考這個問題,想必應該是會出現問題的,難道不是嗎?

答案是,僅僅存在循環參考的情況本身並不會導致問題。如果一個模組已經被引用了,Python可以做到不再次進行引用。但是如果每個模組試圖訪問其他模組定義的函數或變數的時機不對,那麼你就很可能陷入困境。

那麼回到我們的樣本,當我們匯入a.py模組時,它在引用b.py模組時是不會出現問題的,因為b.py模組在被引用時,並不需要訪問在a.py模組中定義的任何變數或函數。b.py模組中對a模組唯一的引用,就是調用了a模組的foo()函數。但是那個函數調用發生在g()函數當中,而a.py或b.py模組中都沒有調用g()函數。所以,不會出現問題。

但是,如果我們試著匯入b.py模組呢(即之前沒有引用a.py模組的前提下):
>>> import b
Traceback (most recent call last):
File "<stdin>"</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的過程中,它試圖引用a.py模組,而a.py模組接著又要調用foo()函數,這個foo()函數接著又試圖去訪問b.x變數。但是這個時候,b.x變數還沒有被定義,所以才出現了AttributeError異常。

解決這個問題有一種非常簡單的方法,就是簡單地修改下b.py模組,在g()函數內部才引用a.py:
x = 1
def g():
import a # This will be evaluated only when g() is called
print a.f()

現在我們再匯入b.py模組的話,就不會出現任何問題了:
>>> 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標準庫模組名衝突

Python語言的一大優勢,就是其本身內建的強大標準庫。但是,正因為如此,如果你不去刻意注意的話,你也是有可能為自己的模組取一個和Python內建標準庫模組相同的名字(例如,如果你的代碼中有一個模組叫email.py,那麼這就會與Python標準庫中同名的模組相衝突。)

這很可能會給你帶來難纏的問題。舉個例子,在匯入模組A的時候,假如該模組A試圖引用Python標準庫中的模組B,但卻因為你已經有了一個同名模組B,模組A會錯誤地引用你自己代碼中的模組B,而不是Python標準庫中的模組B。這也是導致一些嚴重錯誤的原因。

因此,Python程式員要格外注意,避免使用與Python標準庫模組相同的名稱。畢竟,修改自己模組的名稱比提出PEP提議修改上遊模組名稱且讓提議通過,要來得容易的多。
常見錯誤9:未能解決Python 2與Python 3之間的差異

假設有下面這段代碼:
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 <module>
bad()
File "foo.py", line 17, in bad
print(e)
UnboundLocalError: local variable 'e' referenced before assignment

這到底是怎麼回事?這裡的“問題”是,在Python 3中,異常對象在except代碼塊範圍之外是無法訪問的。(這麼設計的原因在於,如果不這樣的話,堆疊框架中就會一直保留它的引用迴圈,直到記憶體回收行程運行,將引用從記憶體中清除。)

避免這個問題的一種方法,就是在except代碼塊的範圍之外,維持一個對異常對象的引用(reference),這樣異常對象就可以訪問了。下面這段代碼就使用了這種方法,因此在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()


在Python 3下運行代碼:
$ python3 foo.py 1
key error
1
$ python3 foo.py 2
value error
2

太棒了!
常見錯誤10:錯誤使用del方法

假設你在mod.py的檔案中編寫了下面的代碼:
import foo
class Bar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)

之後,你在another_mod.py檔案中進行如下操作:
import mod
mybar = mod.Bar()


如果你運行another_mod.py模組的話,將會出現AttributeError異常。

為什嗎?因為當解譯器結束啟動並執行時候,該模組的全域變數都會被設定為None。因此,在上述樣本中,當__del__方法被調用之前,foo已經被設定成了None。

要想解決這個有點棘手的Python編程問題,其中一個辦法就是使用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是一門強大而又靈活的程式設計語言,提供的許多編程機制和範式可以極大地提高工作效率。但是與任何軟體工具或語言一樣,如果對該語言的能力理解有限或無法欣賞,那麼有時候自己反而會被阻礙,而不是受益了。正如一句諺語所說,“自以為知道夠多,但實則會給自己或別人帶來危險。

不斷地熟悉Python語言的一些細微之處,尤其是本文中提到的10大常見錯誤,將會協助你有效地使用這門語言,同時也能避免犯一些比較常見的錯誤。
  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.