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

jopen 10年前發布 | 29K 次閱讀 Python

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

關于Python

Python是一種解釋性、面向對象并具有動態語義的高級程序語言。它內建了高級的數據結構,結合了動態類型和動態綁定的優點,這使得它在快速應用開發中非常有吸引力,并且可作為腳本或膠水語言來連接現有的組件或服務。Python支持模塊和包,從而鼓勵了程序的模塊化和代碼重用。

關于這篇文章

Python簡單易學的語法可能會使Python開發者–尤其是那些編程的初學者–忽視了它的一些微妙的地方并低估了這門語言的能力。

有鑒于此,本文列出了一個“10強”名單,枚舉了甚至是高級Python開發人員有時也難以捕捉的錯誤。

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


常見錯誤 #1: 濫用表達式作為函數參數的默認值

Python允許為函數的參數提供默認的可選值。盡管這是語言的一大特色,但是它可能會導致一些易變默認值的混亂。例如,看一下這個Python函數的定義:

>>> 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被設置為[](即一個空列表)。

但是讓我們看一下這樣做時究竟會發生什么:

>>> foo()
["baz"]>>> foo()
["baz", "baz"]>>> foo()
["baz", "baz", "baz"]

耶?為什么每次foo()調用時都要把默認值"baz"追加到現有列表中而不是創建一個新的列表呢?

答案是函數參數的默認值只會評估使用一次—在函數定義的時候。因此,bar參數在初始化時為其默認值(即一個空列表),即foo()首次定義的時候,但當調用foo()時(即,不指定bar參數時)將繼續使用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

什么 $%#!&?? 我們只改了A.x,為什么C.x也改了?

在Python中,類變量在內部當做字典來處理,其遵循常被引用的方法解析順序(MRO)。所以在上面的代碼中,由于class C中的x屬性沒有找到,它會向上找它的基類(盡管Python支持多重繼承,但上面的例子中只有A)。換句話說,class C中沒有它自己的x屬性,其獨立于A。因此,C.x事實上是A.x的引用。

常見錯誤 #3: 為 except 指定錯誤的參數

假設你有如下一段代碼:

>>> 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都支持這種語法:

>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except (ValueError, IndexError) as e:  
...     pass
...
>>>

常見錯誤 #4:  不理解Python的作用域

Python是基于 LEGB 來進行作用于解析的, LEGB 是 Local, Enclosing, Global, Built-in 的縮寫。看起來“見文知意”,對嗎?實際上,在Python中還有一些需要注意的地方,先看下面一段代碼:

>>> 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 時,這個問題就更加常見.  請看下面這個例子:

>>> 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)

下面代碼中的問題應該是相當明顯的:

>>> 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)針對這個問題是特別有用的,通過更改上文中的實現,得到一段極佳的代碼:

>>> 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)
...

你也許希望獲得下面的輸出結果:

0
2
4
6
8

但實際的結果卻是:

8
8
8
8
8

驚訝吧!

這之所以會發生是由于Python中的“后期綁定”行為——閉包中用到的變量只有在函數被調用的時候才會被賦值。所以,在上面的代碼中,任何時候,當返回的函數被調用時,Python會在該函數被調用時的作用域中查找 i 對應的值(這時,循環已經結束,所以 i 被賦上了最終的值——4)。

解決的方法有一點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

在這里,我們利用了默認參數來生成一個匿名的函數以便實現我們想要的結果。有人說這個方法很巧妙,有人說它難以理解,還有人討厭這種做法。但是,如果你是一個 Python 開發者,理解這種行為很重要。

常見錯誤 #7: 創建循環依賴模塊

讓我們假設你有兩個文件,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:

>>> 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),如下所示:

>>> 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:

x = 1

def g():
    import a    # 只有當g()被調用的時候才會引入a
    print a.f()

現在,當我們再引入b,沒有任何問題:

>>> 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標準庫中的模塊,由于你定義了一個同名的模塊,就會使該模塊錯誤的引入了你的模塊,而不是 stdlib 中的模塊。這就會出問題了。

因此,我們必須要注意這個問題,以避免使用和Python標準庫中相同的模塊名。修改你包中的模塊名要比通過 Python Enhancement Proposal (PEP) 給Python提建議來修改標準庫的模塊名容易多了。

常見錯誤 #9: 未能解決Python 2和Python 3之間的差異

請看下面這個 filefoo.py:

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中運行良好。

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 的文件:

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 的代碼修改成下面的這樣:

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的一些關鍵的細微之處,像本文中所提到的那些(但不限于這些),可以幫助我們更好的去使用語言,從而避免一些常見的陷阱。

你可以查看“Python 面試官指南” 來獲得一些關于如何辨別一個開發者是否是Python專家的建議。

我們希望你在這篇文章中找到了一些對你有幫助的東西,并希望你得到你的反饋。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!