Python程式設計師的常見錯誤

果果夫斯基發表於2014-07-27

譯註:Mark Lutz 是《Learning Python | 學習Python》的作者之一。

在這篇文章中,我將總結新老Python程式設計師常犯的一些錯誤,以幫助你們在自己的工作避免犯同樣或類似錯誤。

首先我要說明一下的是,這些都是來源於第一手的經驗。我以講授Python的知識為生。在過去的7年裡,我已經給上千名學生講授上百堂Python的課程,同時看著這些學生們犯同樣的錯。也就是說,這些是我看著Python初學者活生生犯的錯,千百次的錯。事實上,這些錯誤實在是太普遍了以至於我敢保證你剛開始學的時候是一定會犯的。

“那麼是什麼呢?”你會問,“你也會在Python裡犯那麼多錯麼?”是的。Python可能是最簡單、最靈活的語言之一,但它終究還是一門程式語言。它仍然有語法,資料型別,以及巫師蒂姆居住的黑暗角落。

(典故出自《蒙蒂派森與聖盃》中的魔法師蒂姆,他主角們指點在洞穴的牆壁上記錄的聖盃位置,作者在此處的意思是Python語言裡容易犯錯的地方。另,Python語言得名於作者Guido van Rossum特別喜歡的《蒙蒂派森飛行馬戲團(Monty Python’s Flying Circus)》——譯者注)

好事情是多虧了Python那乾淨的設計,一旦你學會了Python,你就能自動的避開很多陷阱。Python在其各元件之間有著最小的互動,這能有效的減少bug。它也擁有十分簡單的語法,這意味著在一開始你就有更小的概率犯錯。當你實在是犯了錯的時候,Python的即時錯誤檢測和報告能幫你迅速的恢復。

但用Python程式設計也不是個自動完成的活兒,很多事還是要早做準備。那麼廢話不多說了,讓我們直切正題。在接下來的三節裡我們將這些錯誤分為語用、程式碼,以及程式設計三個大類。如果你想讀到更多的Python的常見錯誤以及如何避免它們,那麼在O’Reilly系列叢書的《Learning Python》裡有詳細的解讀。(譯註:Learning Python 已經是第五版了)

 

語用錯誤

讓我們從基礎開始,從那些剛學習程式設計的人鑽研語法之前碰到的事情開始。如果你已經編過一些程了,那麼以下這些可能看起來十分的簡單;如果你曾經嘗試過教新手們怎麼程式設計,它們可能就不這麼簡單了。

在互動提示符中輸入Python程式碼

在>>>互動提示符中你只能輸入Python程式碼,而不是系統命令。時常有人在這個提示符下輸入emacs,ls,或者edit之類的命令,這些可不是Python程式碼。在Python程式碼中確實有辦法來呼叫系統命令(例如os.system和os.popen),但可不是像直接輸入命令這麼直接。如果你想要在互動提示符中啟動一個Python檔案,請用import file,而不是系統命令python file.py。

Print語句(僅僅)是在檔案中需要

因為互動直譯器會自動的講表示式的結果輸出,所以你不需要互動的鍵入完整的print語句。這是個很棒的功能,但是記住在程式碼檔案裡,通常你只有用print語句才能看得到輸出。

小心Windows裡的自動副檔名

如果你在Windows裡使用記事本來編輯程式碼檔案的話,當你保持的時候小心選擇“所有檔案”(All Files)這個型別,並且明確的給你的檔案加一個.py的字尾。不然的話記事本會給你的檔案加一個.txt的副檔名,使得在某些啟動方法中沒法跑這個程式。更糟糕的是,像Word或者是寫字板一類的文書處理軟體還會預設的加上一些格式字元,而這些字元Python語法是不認的。所以記得,在Windows下總是選“所有檔案”(All Files),並儲存為純文字,或者使用更加“程式設計友好”的文字編輯工具,比如IDLE。在IDLE中,記得在儲存時手動加上.py的副檔名。

在Windows下點選圖示的問題

在Windows下,你能靠點選Python檔案來啟動一個Python程式,但這有時會有問題。首先,程式的輸出視窗在程式結束的瞬間也就消失了,要讓它不消失,你可以在檔案最後加一條raw_input()的呼叫。另外,記住如果有錯的話,輸出視窗也就立即消失了。要看到你的錯誤資訊的話,用別的方法來呼叫你的程式:比如從系統命令列啟動,通過提示符下用import語句,或者IDLE選單裡的選項,等等。

Import只在第一次有效

你可以在互動提示符中通過import一個檔案來執行它,但是這隻會在一個會話中起一次作用;接下來的import僅僅是返回這個已經載入的模組。要想強制Python重新載入一個檔案的程式碼,請呼叫函式reload(module)來達到這個目的。注意對reload請使用括號,而import不要使用括號。

空白行(僅僅)在互動提示符中有作用

在模組檔案中空白行和註釋統統會被忽略掉,但是在互動提示符中鍵入程式碼時,空白行表示一個複合語句的結束。換句話說,空白行告訴互動提示符你完成了一個複合語句;在你真正完成之前不要鍵入回車。事實上當你要開始一個新的語句時,你需要鍵入一個空行來結束當前的語句——互動提示符一次只執行一條語句。

 

程式碼錯誤

一旦你開始認真寫Python程式碼了,接下來了一堆陷阱就更加危險了——這些都是一些跨語言特性的基本程式碼錯誤,並常常困擾不細心的程式設計師。

別忘了冒號

這是新手程式設計師最容易犯的一個錯誤:別忘了在複合語句的起始語句(if,while, for等語句的第一行)結束的地方加上一個冒號“:”。也許你剛開始會忘掉這個,但是到了很快這就會成為一個下意識的習慣。課堂裡75%的學生當天就可以記住這個。

初始化變數

在Python裡,一個表示式中的名字在它被賦值之前是沒法使用的。這是有意而為的:這樣能避免一些輸入失誤,同時也能避免預設究竟應該是什麼型別的問題(0,None,””,[],?)。記住把計數器初始化為0,列表初始化為[],以此類推。

從第一列開始

確保把頂層的,未巢狀的程式碼放在最左邊第一列開始。這包括在模組檔案中未巢狀的程式碼,以及在互動提示符中未巢狀的程式碼。Python使用縮排的辦法來區分巢狀的程式碼段,因此在你程式碼左邊的空格意味著巢狀的程式碼塊。除了縮排以外,空格通常是被忽略掉的。

縮排一致

在同一個程式碼塊中避免講tab和空格混用來縮排,除非你知道執行你的程式碼的系統是怎麼處理tab的。否則的話,在你的編輯器裡看起來是tab的縮排也許Python看起來就會被視作是一些空格。保險起見,在每個程式碼塊中全都是用tab或者全都是用空格來縮排;用多少由你決定。

在函式呼叫時使用括號

無論一個函式是否需要引數,你必須要加一對括號來呼叫它。即,使用function(),而不是function。Python的函式簡單來說是具有特殊功能(呼叫)的物件,而呼叫是用括號來觸發的。像所有的物件一樣,他們也可以被賦值給變數,並且間接的使用他們:x=function:x()。
在Python的培訓中,這樣的錯誤常常在檔案的操作中出現。通常會看到新手用file.close來關閉一個問題,而不是用file.close()。因為在Python中引用一個函式而不呼叫它是合法的,因此不使用括號的操作(file.close)無聲的成功了,但是並沒有關閉這個檔案!

在Import時不要使用表示式或者路徑

在系統的命令列裡使用資料夾路徑或者檔案的副檔名,但不要在import語句中使用。即,使用import mod,而不是import mod.py,或者import dir/mod.py。在實際情況中,這大概是初學者常犯的第二大錯誤了。因為模組會有除了.py以為的其他的字尾(例如,.pyc),強制寫上某個字尾不僅是不合語法的,也沒有什麼意義。
和系統有關的目錄路徑的格式是從你的模組搜尋路徑的設定裡來的,而不是import語句。你可以在檔名裡使用點來指向包的子目錄(例如,import dir1.dir2.mod),但是最左邊的目錄必須得通過模組搜尋路徑能夠找到,並且沒有在import中沒有其他路徑格式。不正確的語句import mod.py被Python認為是要記在一個包,它先載入一個模組mod,然後試圖通過在一個叫做mod的目錄裡去找到叫做py的模組,最後可能什麼也找不到而報出一系列費解的錯誤資訊。

不要在Python中寫C程式碼

以下是給不熟悉Python的C程式設計師的一些備忘貼士:

  • 在if和while中條件測試時,不用輸入括號(例如,if (X==1):)。如果你喜歡的話,加上括號也無妨,只是在這裡是完全多餘的。
  • 不要用分號來結束你的語句。從技術上講這在Python裡是合法的,但是這毫無用處,除非你要把很多語句放在同一行裡(例如,x=1; y=2; z=3)。
  • 不要在while迴圈的條件測試中嵌入賦值語句(例如,while ((x=next() != NULL))。在Python中,需要表示式的地方不能出現語句,並且賦值語句不是一個表示式。

 

程式設計錯誤

下面終於要講到當你用到更多的Python的功能(資料型別,函式,模組,類等等)時可能碰到的問題了。由於篇幅有限,這裡儘量精簡,尤其是對一些高階的概念。要想了解更多的細節,敬請閱讀Learning Python, 2nd Edition的“小貼士”以及“Gotchas”章節。

開啟檔案的呼叫不使用模組搜尋路徑

當你在Python中呼叫open()來訪問一個外部的檔案時,Python不會使用模組搜尋路徑來定位這個目標檔案。它會使用你提供的絕對路徑,或者假定這個檔案是在當前工作目錄中。模組搜尋路徑僅僅為模組載入服務的。

不同的型別對應的方法也不同

列表的方法是不能用在字串上的,反之亦然。通常情況下,方法的呼叫是和資料型別有關的,但是內部函式通常在很多型別上都可以使用。舉個例子來說,列表的reverse方法僅僅對列表有用,但是len函式對任何具有長度的物件都適用

不能直接改變不可變資料型別

記住你沒法直接的改變一個不可變的物件(例如,元組,字串):

用切片,聯接等構建一個新的物件,並根據需求將原來變數的值賦給它。因為Python會自動回收沒有用的記憶體,因此這沒有看起來那麼浪費:

使用簡單的for迴圈而不是while或者range

當你要從左到右遍歷一個有序的物件的所有元素時,用簡單的for迴圈(例如,for x in seq:)相比於基於while-或者range-的計數迴圈而言會更容易寫,通常執行起來也更快。除非你一定需要,儘量避免在一個for迴圈裡使用range:讓Python來替你解決標號的問題。在下面的例子中三個迴圈結構都沒有問題,但是第一個通常來說更好;在Python裡,簡單至上。

不要試圖從那些會改變物件的函式得到結果

諸如像方法list.append()和list.sort()一類的直接改變操作會改變一個物件,但不會將它們改變的物件返回出來(它們會返回None);正確的做法是直接呼叫它們而不要將結果賦值。經常會看見初學者會寫諸如此類的程式碼:

目的是要得到append的結果,但是事實上這樣做會將None賦值給mylist,而不是改變後的列表。更加特別的一個例子是想通過用排序後的鍵值來遍歷一個字典裡的各個元素,請看下面的例子:

差一點兒就成功了——keys方法會建立一個keys的列表,然後用sort方法來將這個列表排序——但是因為sort方法會返回None,這個迴圈會失敗,因為它實際上是要遍歷None(這可不是一個序列)。要改正這段程式碼,將方法的呼叫分離出來,放在不同的語句中,如下:

只有在數字型別中才存在型別轉換

在Python中,一個諸如123+3.145的表示式是可以工作的——它會自動將整數型轉換為浮點型,然後用浮點運算。但是下面的程式碼就會出錯了:

這同樣也是有意而為的,因為這是不明確的:究竟是將字串轉換為數字(進行相加)呢,還是將數字轉換為字串(進行聯接)呢?在Python中,我們認為“明確比含糊好”(即,EIBTI(Explicit is better than implicit)),因此你得手動轉換型別:

迴圈的資料結構會導致迴圈

儘管這在實際情況中很少見,但是如果一個物件的集合包含了到它自己的引用,這被稱為迴圈物件(cyclic object)。如果在一個物件中發現一個迴圈,Python會輸出一個[…],以避免在無限迴圈中卡住:

除了知道這三個點在物件中表示迴圈以外,這個例子也是很值得借鑑的。因為你可能無意間在你的程式碼中出現這樣的迴圈的結構而導致你的程式碼出錯。如果有必要的話,維護一個列表或者字典來表示已經訪問過的物件,然後通過檢查它來確認你是否碰到了迴圈。

賦值語句不會建立物件的副本,僅僅建立引用

這是Python的一個核心理念,有時候當行為不對時會帶來錯誤。在下面的例子中,一個列表物件被賦給了名為L的變數,然後L又在列表M中被引用。內部改變L的話,同時也會改變M所引用的物件,因為它們倆都指向同一個物件。

通常情況下只有在稍大一點的程式裡這就顯得很重要了,而且這些共用的引用通常確實是你需要的。如果不是的話,你可以明確的給他們建立一個副本來避免共用的引用;對於列表來說,你可以通過使用一個空列表的切片來建立一個頂層的副本:

切片的範圍起始從預設的0到被切片的序列的最大長度。如果兩者都省略掉了,那麼切片會抽取該序列中的所有元素,並創造一個頂層的副本(一個新的,不被公用的物件)。對於字典來說,使用字典的dict.copy()方法。

靜態識別本地域的變數名

Python預設將一個函式中賦值的變數名視作是本地域的,它們存在於該函式的作用域中並且僅僅在函式執行的時候才存在。從技術上講,Python是在編譯def程式碼時,去靜態的識別本地變數,而不是在執行時碰到賦值的時候才識別到的。如果不理解這點的話,會引起人們的誤解。比如,看看下面的例子,當你在一個引用之後給一個變數賦值會怎麼樣:

你會得到一個“未定義變數名”的錯誤,但是其原因是很微妙的。當編譯這則程式碼時,Python碰到給X賦值的語句時認為在這個函式中的任何地方X會被視作一個本地變數名。但是之後當真正執行這個函式時,執行print語句的時候,賦值語句還沒有發生,這樣Python便會報告一個“未定義變數名”的錯誤。

事實上,之前的這個例子想要做的事情是很模糊的:你是想要先輸出那個全域性的X,然後建立一個本地的X呢,還是說這是個程式的錯誤?如果你真的是想要輸出這個全域性的X,你需要將它在一個全域性語句中宣告它,或者通過包絡模組的名字來引用它。

預設引數和可變物件

在執行def語句時,預設引數的值只被解析並儲存一次,而不是每次在呼叫函式的時候。這通常是你想要的那樣,但是因為預設值需要在每次呼叫時都保持同樣物件,你在試圖改變可變的預設值(mutable defaults)的時候可要小心了。例如,下面的函式中使用一個空的列表作為預設值,然後在之後每一次函式呼叫的時候改變它的值:

有的人將這個視作Python的一個特點——因為可變的預設引數在每次函式呼叫時保持了它們的狀態,它們能提供像C語言中靜態本地函式變數的類似的一些功能。但是,當你第一次碰到它時會覺得這很奇怪,並且在Python中有更加簡單的辦法來在不同的呼叫之間儲存狀態(比如說類)。

要擺脫這樣的行為,在函式開始的地方用切片或者方法來建立預設引數的副本,或者將預設值的表示式移到函式裡面;只要每次函式呼叫時這些值在函式裡,就會每次都得到一個新的物件:

其他常見的程式設計陷阱

下面列舉了其他的一些在這裡沒法詳述的陷阱:

  • 在頂層檔案中語句的順序是有講究的:因為執行或者載入一個檔案會從上到下執行它的語句,所以請確保將你未巢狀的函式呼叫或者類的呼叫放在函式或者類的定義之後。
  • reload不影響用from載入的名字:reload最好和import語句一起使用。如果你使用from語句,記得在reload之後重新執行一遍from,否則你仍然使用之前老的名字。
  • 在多重繼承中混合的順序是有講究的:這是因為對superclass的搜尋是從左到右的,在類定義的頭部,在多重superclass中如果出現重複的名字,則以最左邊的類名為準。
  • 在try語句中空的except子句可能會比你預想的捕捉到更多的錯誤。在try語句中空的except子句表示捕捉所有的錯誤,即便是真正的程式錯誤,和sys.exit()呼叫,也會被捕捉到。
  • 兔子可能會比他們看起來更加危險。(原句Bunnies can be more dangerous than they seem. 意思是一些看起來比較細微的問題實際上可能更危險。——譯者注)

作者Mark Lutz繫世界領先的Python教育者,Python最早的暢銷教材的作者,並且從1992年開始便長期貢獻於Python社群。

相關文章