Python 程序員常常犯的 10 個錯誤

關於PythonPython是一種解釋性、面向對象並具備動態語義的高級程序語言。它內建了高級的數據結構,結合了動態類型和動態綁定的優勢,這使得...
關於Python
Python是一種解釋性、面向對象並具備動態語義的高級程序語言。它內建了高級的數據結構,結合了動態類型和動態綁定的優勢,這使得它在快速應用開發中很是有吸引力,而且可做爲腳本或膠水語言來鏈接現有的組件或服務。Python支持模塊和包,從而鼓勵了程序的模塊化和代碼重用。
關於這篇文章
Python簡單易學的語法可能會使Python開發者–尤爲是那些編程的初學者–忽視了它的一些微妙的地方並低估了這門語言的能力。
有鑑於此,本文列出了一個「10強」名單,枚舉了甚至是高級Python開發人員有時也難以捕捉的錯誤。
常見錯誤 1: 濫用表達式做爲函數參數的默認值
Python容許爲函數的參數提供默認的可選值。儘管這是語言的一大特點,可是它可能會致使一些易變默認值的混亂。例如,看一下這個Python函數的定義:
1
2
3
>>> 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 bar
一個常見的錯誤是認爲在函數每次不提供可選參數調用時可選參數將設置爲默認指定值。在上面的代碼中,例如,人們可能會但願反覆(即不明確指定bar參數)地調用foo()時總返回'baz',因爲每次foo()調用時都假定(不設定bar參數)bar被設置爲[](即一個空列表)。
可是讓咱們看一下這樣作時究竟會發生什麼:
1
2
3
4
>>> foo()  
["baz"]>>> foo()  
["baz", "baz"]>>> foo()  
["baz", "baz", "baz"]
耶?爲何每次foo()調用時都要把默認值"baz"追加到現有列表中而不是建立一個新的列表呢?
答案是函數參數的默認值只會評估使用一次—在函數定義的時候。所以,bar參數在初始化時爲其默認值(即一個空列表),即foo()首次定義的時候,但當調用foo()時(即,不指定bar參數時)將繼續使用bar本來已經初始化的參數。
下面是一個常見的解決方法:
1
2
3
4
5
6
7
8
9
10
11
12
>>> 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: 錯誤地使用類變量
考慮一下下面的例子:
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
常規用一下。
1
2
3
>>> B.x = 2 
>>> print A.x, B.x, C.x  
1 2 1
嗯,再試一下也同樣。
1
2
3
>>> 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 指定錯誤的參數
假設你有以下一段代碼:
1
2
3
4
5
6
7
8
9
>>> 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都支持這種語法:
1
2
3
4
5
6
7
>>> try:  
...     l = ["a", "b"]  
...     int(l[2])  
... except (ValueError, IndexError) as e:    
...     pass 
...  
>>>
常見錯誤 4:  不理解Python的做用域
Python是基於 LEGB 來進行做用於解析的, LEGB 是 Local, Enclosing, Global, Built-in 的縮寫。看起來「見文知意」,對嗎?實際上,在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 時,這個問題就更加常見.  請看下面這個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> 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)
下面代碼中的問題應該是至關明顯的:
1
2
3
4
5
6
>>> 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  
當迭代的時候,從一個 列表 (List)或者數組中刪除元素,對於任何有經驗的開發者來講,這是一個衆所周知的錯誤。儘管上面的例子很是明顯,可是許多高級開發者在更復雜的代碼中也並不是是故意而爲之的。
幸運的是,Python包含大量簡潔優雅的編程範例,若使用得當,能大大簡化和精煉代碼。這樣的好處是能獲得更簡化和更精簡的代碼,能更好的避免程序中出現當迭代時修改一個列表(List)這樣的bug。一個這樣的範例是遞推式列表(list comprehensions)。並且,遞推式列表(list comprehensions)針對這個問題是特別有用的,經過更改上文中的實現,獲得一段極佳的代碼:
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)]  # ahh, the beauty of it all  
>>> numbers  
[0, 2, 4, 6, 8]
常見錯誤 6: 不明白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)  
...
你也許但願得到下面的輸出結果:
0 
2 
4 
6 
8 
但實際的結果倒是:
8  
8  
8  
8  
8  
驚訝吧!
這之因此會發生是因爲Python中的「後期綁定」行爲——閉包中用到的變量只有在函數被調用的時候纔會被賦值。因此,在上面的代碼中,任什麼時候候,當返回的函數被調用時,Python會在該函數被調用時的做用域中查找 i 對應的值(這時,循環已經結束,因此 i 被賦上了最終的值——4)。
解決的方法有一點hack的味道:
1
2
3
4
5
6
>>> 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 開發者,理解這種行爲很重要。
常見錯誤 7: 建立循環依賴模塊
讓咱們假設你有兩個文件,a.py 和 b.py,他們之間相互引用,以下所示:
a.py:
1
2
3
4
import b  
def f():  
    return b.x    
print f()
b.py:
1
2
3
4
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),以下所示:
1
>>> 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:
1
2
3
4
x = 1 
def g():  
    import a    # 只有當g()被調用的時候纔會引入a  
    print a.f()
如今,當咱們再引入b,沒有任何問題:
1
2
3
4
>>> 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'
1
常見錯誤 8: 與Python標準庫中的模塊命名衝突
Python一個使人稱讚的地方是它有豐富的模塊可供咱們「開箱即用」。可是,若是你沒有有意識的注意的話,就很容易出現你寫的模塊和Python自帶的標準庫的模塊之間發生命名衝突的問題(如,你也許有一個叫 email.py 的模塊,但這會和標準庫中的同名模塊衝突)。
這可能會致使很怪的問題,例如,你引入了另外一個模塊,但這個模塊要引入一個Python標準庫中的模塊,因爲你定義了一個同名的模塊,就會使該模塊錯誤的引入了你的模塊,而不是 stdlib 中的模塊。這就會出問題了。
所以,咱們必需要注意這個問題,以免使用和Python標準庫中相同的模塊名。修改你包中的模塊名要比經過 Python Enhancement Proposal (PEP) 給Python提建議來修改標準庫的模塊名容易多了。
常見錯誤 #9: 未能解決Python 2和Python 3之間的差別
請看下面這個 filefoo.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 代碼塊的外部做用域中定義一個對異常對象的引用,以便訪問。下面的例子使用了該方法,所以最後的代碼能夠在Python 2 和 Python 3中運行良好。
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 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 的文件:
1
2
3
4
5
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 的代碼修改爲下面的這樣:
1
2
3
4
5
6
7
8
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的一些關鍵的細微之處,像本文中所提到的那些(但不限於這些),能夠幫助咱們更好的去使用語言,從而避免一些常見的陷阱。
相關文章
相關標籤/搜索