錯誤的 Python 代碼:Python 開發人員最常犯的 10 個錯誤

已發表: 2022-03-11

關於 Python

Python 是一種具有動態語義的解釋型、面向對象的高級編程語言。 它的高級內置數據結構與動態類型和動態綁定相結合,使其對於快速應用程序開發以及用作連接現有組件或服務的腳本或膠水語言非常有吸引力。 Python 支持模塊和包,從而鼓勵程序模塊化和代碼重用。

關於這篇文章

Python 簡單易學的語法可能會誤導 Python 開發人員——尤其是那些剛接觸該語言的開發人員——忽略它的一些微妙之處,並低估了各種 Python 語言的力量。

考慮到這一點,本文提出了一個“前 10 名”列表,其中列出了一些微妙的、難以發現的錯誤,這些錯誤甚至可能會在後面咬一些更高級的 Python 開發人員。

(注意:這篇文章的目標讀者是比 Python 程序員的常見錯誤更高級的讀者,後者更適合那些剛接觸這門語言的人。)

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

Python 允許您通過為其提供默認值來指定函數參數是可選的。 雖然這是該語言的一個很棒的特性,但當默認值是mutable時,它可能會導致一些混亂。 例如,考慮這個 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

一個常見的錯誤是認為每次調用函數時都將可選參數設置為指定的默認表達式,而沒有為可選參數提供值。 例如,在上面的代碼中,人們可能期望重複調用foo() (即,不指定bar參數)將始終返回'baz' ,因為假設每次調用foo()時(沒有bar參數指定) bar設置為[] (即,一個新的空列表)。

但是讓我們看看當你這樣做時實際發生了什麼:

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

嗯? 為什麼每次調用foo()時都將"baz"的默認值附加到現有列表,而不是每次都創建一個列表?

更高級的 Python 編程答案是函數參數的默認值僅在定義函數時計算一次。 因此, 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 Ax, Bx, Cx 1 1 1

說得通。

 >>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1

是的,正如預期的那樣。

 >>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3

$%#!&是什麼? 我們只改變了Ax 。 為什麼Cx也發生了變化?

在 Python 中,類變量在內部作為字典處理,並遵循通常所說的方法解析順序 (MRO)。 所以在上面的代碼中,由於在類C中沒有找到屬性x ,所以會在它的基類中查找(上例中只有A ,雖然 Python 支持多重繼承)。 換句話說, C沒有自己的x屬性,獨立於A 。 因此,對Cx的引用實際上是對Ax的引用。 除非處理得當,否則這會導致 Python 問題。 詳細了解 Python 中的類屬性。

常見錯誤 #3:錯誤地為異常塊指定參數

假設您有以下代碼:

 >>> 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 規則,它是L ocal、Enclosure、 G lobal、 B uilt -in 的簡寫。 看起來很簡單,對吧? 好吧,實際上,它在 Python 中的工作方式有一些微妙之處,這將我們帶到下面常見的更高級的 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 。 (您可以在此處閱讀有關此內容的更多信息。)

在使用列表時,這特別常見於絆倒開發人員。 考慮以下示例:

 >>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> 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

嗯? 為什麼foo1運行良好時foo2會爆炸?

答案與前面的示例問題相同,但無疑更微妙。 foo1沒有對lst進行分配,而foo2是。 記住lst += [5]實際上只是lst = lst + [5]的簡寫,我們看到我們正在嘗試為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>", 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) ...

您可能期望以下輸出:

 0 2 4 6 8

但你實際上得到:

 8 8 8 8 8

驚喜!

發生這種情況是由於 Python 的後期綁定行為,即在調用內部函數時查找閉包中使用的變量的值。 所以在上面的代碼中,每當調用任何返回的函數時,都會在調用時在周圍的範圍內查找i的值(到那時,循環已經完成,所以i已經被分配了它的 final值 4)。

這個常見的 Python 問題的解決方案有點小技巧:

 >>> 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.pyb.py ,每個文件都導入另一個文件,如下所示:

a.py

 import b def f(): return bx print f()

b.py中:

 import a x = 1 def g(): print af()

首先,讓我們嘗試導入a.py

 >>> import a 1

工作得很好。 也許這讓你感到驚訝。 畢竟,我們在這裡確實有一個循環導入,這大概應該是一個問題,不是嗎?

答案是循環導入的存在本身並不是 Python 中的問題。 如果一個模塊已經被導入,Python 足夠聰明,不會嘗試重新導入它。 但是,根據每個模塊嘗試訪問另一個模塊中定義的函數或變量的點,您可能確實會遇到問題。

回到我們的示例,當我們導入a.py時,導入b.py沒有問題,因為b.py不需要在導入時定義a.py中的任何內容。 b.py中對a的唯一引用是對af()的調用。 但是該調用在g()中,並且a.pyb.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 bx AttributeError: 'module' object has no attribute 'x'

哦哦。 這不好! 這裡的問題是,在導入b.py的過程中,它嘗試導入a.py ,這反過來又調用f() ,它試圖訪問bx 。 但是bx還沒有被定義。 因此出現AttributeError異常。

至少有一個解決方案是非常簡單的。 只需修改b.pyg()中導入a.py

 x = 1 def g(): import a # This will be evaluated only when g() is called print af()

不,當我們導入它時,一切都很好:

 >>> import b >>> bg() 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 標準庫版本,但是,由於您有一個具有相同名稱的模塊,另一個包錯誤地導入了您的版本而不是其中的版本Python 標準庫。 這就是發生嚴重 Python 錯誤的地方。

因此,應注意避免使用與 Python 標準庫模塊中的名稱相同的名稱。 更改包中模塊的名稱比提交 Python 增強提案 (PEP) 以請求上游更改名稱並嘗試獲得批准要容易得多。

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

考慮以下文件foo.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 招聘指南討論了在將代碼從 Python 2 遷移到 Python 3 時需要注意的許多其他重要差異。)

常見錯誤 #10:濫用__del__方法

假設你在一個名為mod.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

這個更高級的 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 的關鍵細微差別,例如(但不限於)本文中提出的中等高級編程問題,將有助於優化該語言的使用,同時避免一些更常見的錯誤。

您可能還想查看我們的 Python 面試內幕指南,以獲取有關有助於識別 Python 專家的面試問題的建議。

我們希望您發現本文中的建議對您有所幫助,並歡迎您提供反饋。