當前位置:
首頁 > 知識 > 寫 Python 時你要避免的十個錯誤

寫 Python 時你要避免的十個錯誤

推薦 | 編程派公眾號(ID:codingpy)

不管是在學習還是工作過程中,人都會犯錯。雖然Python的語法簡單、靈活,但也一樣存在一些不小的坑,一不小心,初學者和資深Python程序員都有可能會栽跟頭。本文是Toptal網站的程序員梳理的10大常見錯誤,非常有參考意義。大家在開發過程中需要格外注意。

常見錯誤1:錯誤地將表達式作為函數的默認參數

在Python中,我們可以為函數的某個參數設置默認值,使該參數成為可選參數。雖然這是一個很好的語言特性,但是當默認值是可變類型時,也會導致一些令人困惑的情況。我們來看看下面這個Python函數定義:

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

但是,實際運行結果卻是這樣的:

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

答案就是,可選參數默認值的設置在Python中只會被執行一次,也就是定義該函數的時候。因此,只有當 函數被定義時, 參數才會被初始化為默認值(也就是,一個空列表),但是之後每次 函數被調用時,都會繼續使用 參數原先初始化生成的那個列表。

當然,一個常見的解決辦法就是:

常見問題2:錯誤地使用類變數

我們來看下面這個例子:

這個結果很正常。

嗯,結果和預計的一樣。

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

常見錯誤3:錯誤地指定異常代碼塊(exception block)的參數

請看下面這段代碼:

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

要想在 語句中正確地捕獲多個異常,則應將第一個參數指定為元組,然後在元組中寫下希望捕獲的異常類型。另外,為了提高可移植性,請使用 關鍵詞,Python 2和Python 3均支持這種用法。

常見錯誤4:錯誤理解Python中的變數名解析

Python中的變數名解析遵循所謂的 原則,也就是「L:本地作用域;E:上一層結構中def或lambda的本地作用域;G:全局作用域;B:內置作用域」(Local,Enclosing,Global,Builtin),按順序查找。看上去是不是很簡單?不過,事實上這個原則的生效方式還是有著一些特殊之處。說到這點,我們就不得不提下面這個常見的Python編程錯誤。請看下面的代碼:

出了什麼問題?

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

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

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

請看下面這個代碼示例:

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

答案與上一個示例相同,但是卻更難捉摸清楚。 函數並沒有為 變數進行賦值,但是 卻有賦值。我們知道, 只是 的簡寫,從中我們就可以看出, 函數在嘗試為 賦值(因此,被Python解釋器認為是函數本地作用域的變數)。但是,我們希望為 賦的值卻又是基於 變數本身(這時,也被認為是函數本地作用域內的變數),也就是說該變數還沒有被定義。這才出現了錯誤。

常見錯誤5:在遍歷列表時更改列表

下面這段代碼的問題應該算是十分明顯:

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

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

常見錯誤6:不理解Python在閉包中如何綁定變數

請看下面這段代碼:

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

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

嚇了一跳吧!

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

要解決這個常見Python問題的方法中,需要使用一些hack技巧:

請注意!我們在這裡利用了默認參數來實現這個lambda匿名函數。有人可能認為這樣做很優雅,有人會覺得很巧妙,還有人會嗤之以鼻。但是,如果你是一名Python程序員,不管怎樣你都應該要了解這種解決方法。

常見錯誤7:模塊之間出現循環依賴(circular dependencies)

假設你有兩個文件,分別是 和 ,二者相互引用,如下所示:

文件中的代碼:

文件中的代碼:

首先,我們嘗試導入 模塊:

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

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

那麼回到我們的示例,當我們導入 模塊時,它在引用 模塊時是不會出現問題的,因為 模塊在被引用時,並不需要訪問在 模塊中定義的任何變數或函數。 模塊中對a模塊唯一的引用,就是調用了a模塊的 函數。但是那個函數調用發生在 函數當中,而 或 模塊中都沒有調用 函數。所以,不會出現問題。

但是,如果我們試著導入 模塊呢(即之前沒有引用 模塊的前提下):

糟糕。情況不太妙!這裡的問題是,在導入 的過程中,它試圖引用 模塊,而 模塊接著又要調用 函數,這個 函數接著又試圖去訪問 變數。但是這個時候, 變數還沒有被定義,所以才出現了AttributeError異常。

解決這個問題有一種非常簡單的方法,就是簡單地修改下 模塊,在 函數內部才引用 :

現在我們再導入 模塊的話,就不會出現任何問題了:

常見錯誤8:模塊命名與Python標準庫模塊名衝突

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

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

因此,Python程序員要格外注意,避免使用與Python標準庫模塊相同的名稱。畢竟,修改自己模塊的名稱比提出PEP提議修改上游模塊名稱且讓提議通過,要來得容易的多。

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

假設有下面這段代碼:

如果是Python 2,那麼代碼運行正常:

但是現在,我們換成Python 3再運行一遍:

這到底是怎麼回事?這裡的「問題」是,在Python 3中,異常對象在 代碼塊作用域之外是無法訪問的。(這麼設計的原因在於,如果不這樣的話,堆棧幀中就會一直保留它的引用循環,直到垃圾回收器運行,將引用從內存中清除。)

避免這個問題的一種方法,就是在 代碼塊的作用域之外,維持一個對異常對象的引用(reference),這樣異常對象就可以訪問了。下面這段代碼就使用了這種方法,因此在Python 2和Python 3中的輸出結果是一致的:

在Python 3下運行代碼:

太棒了!

常見錯誤10:錯誤使用del方法

假設你在 的文件中編寫了下面的代碼:

之後,你在 文件中進行如下操作:

如果你運行 模塊的話,將會出現AttributeError異常。

為什麼?因為當解釋器結束運行的時候,該模塊的全局變數都會被設置為 。因此,在上述示例中,當 方法被調用之前, 已經被設置成了 。

要想解決這個有點棘手的Python編程問題,其中一個辦法就是使用 方法。這樣的話,當你的程序執行完成之後(即正常退出程序的情況下),你所指定的處理程序就會在解釋器關閉之前運行。

應用了上面這種方法,修改後的 文件可能會是這樣子的:

這種實現支持在程序正常終止時乾淨利落地調用任何必要的清理功能。很明顯,上述示例中將會由 函數來決定如何處理 所綁定的對象。

綜述

Python是一門強大而又靈活的編程語言,提供的許多編程機制和範式可以極大地提高工作效率。但是與任何軟體工具或語言一樣,如果對該語言的能力理解有限或無法欣賞,那麼有時候自己反而會被阻礙,而不是受益了。正如一句諺語所說,「自以為知道夠多,但實則會給自己或別人帶來危險」(knowing enough to be dangerous)。(譯者注:這句諺語的意思是,自以為已經對某件事情了解足夠,但在實際去執行或實施時,卻會給自己和別人帶來危險。)

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

譯者:EarlGrey@編程派

回復下方「關鍵詞」,獲取優質資源

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 編程派 的精彩文章:

程序員寫作必備神器
Netflix 背後的大功臣是 Python!

TAG:編程派 |