天天看點

Mark Lutz: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函數對任何具有長度的對象都适用

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

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

T = (1, 2, 3)

T[2] = 4          # 錯誤

用切片,聯接等建構一個新的對象,并根據需求将原來變量的值賦給它。因為Python會自動回收沒有用的記憶體,是以這沒有看起來那麼浪費:

T = T[:2] + (4,)  # 沒問題了: T 變成了 (1, 2, 4)

使用簡單的for循環而不是while或者range

當你要從左到右周遊一個有序的對象的所有元素時,用簡單的for循環(例如,for x in seq:)相比于基于while-或者range-的計數循環而言會更容易寫,通常運作起來也更快。除非你一定需要,盡量避免在一個for循環裡使用range:讓Python來替你解決标号的問題。在下面的例子中三個循環結構都沒有問題,但是第一個通常來說更好;在Python裡,簡單至上。

S = "lumberjack"

for c in S: print c                   # 最簡單

for i in range(len(S)): print S[i]    # 太多了

i = 0                                 # 太多了

while i < len(S): print S[i]; i += 1

不要試圖從那些會改變對象的函數得到結果

諸如像方法list.append()和list.sort()一類的直接改變操作會改變一個對象,但不會将它們改變的對象傳回出來(它們會傳回None);正确的做法是直接調用它們而不要将結果指派。經常會看見初學者會寫諸如此類的代碼:

mylist = mylist.append(X)

目的是要得到append的結果,但是事實上這樣做會将None指派給mylist,而不是改變後的清單。更加特别的一個例子是想通過用排序後的鍵值來周遊一個字典裡的各個元素,請看下面的例子:

D = {...}

for k in D.keys().sort(): print D[k]

差一點兒就成功了——keys方法會建立一個keys的清單,然後用sort方法來将這個清單排序——但是因為sort方法會傳回None,這個循環會失敗,因為它實際上是要周遊None(這可不是一個序列)。要改正這段代碼,将方法的調用分離出來,放在不同的語句中,如下:

Ks = D.keys()

Ks.sort()

for k in Ks: print D[k]

隻有在數字類型中才存在類型轉換

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

S = "42"

I = 1

X = S + I        # 類型錯誤

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

X = int(S) + I   # 做加法: 43

X = S + str(I)   # 字元串聯接: "421"

循環的資料結構會導緻循環

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

>>> L = ['grail']  # 在 L中又引用L自身會

>>> L.append(L)    # 在對象中創造一個循環

>>> L

['grail', [...]]

除了知道這三個點在對象中表示循環以外,這個例子也是很值得借鑒的。因為你可能無意間在你的代碼中出現這樣的循環的結構而導緻你的代碼出錯。如果有必要的話,維護一個清單或者字典來表示已經通路過的對象,然後通過檢查它來确認你是否碰到了循環。

指派語句不會建立對象的副本,僅僅建立引用

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

>>> L = [1, 2, 3]        # 共用的清單對象

>>> M = ['X', L, 'Y']    # 嵌入一個到L的引用

>>> M

['X', [1, 2, 3], 'Y']

>>> L[1] = 0             # 也改變了M

['X', [1, 0, 3], 'Y']

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

>>> L = [1, 2, 3]

>>> M = ['X', L[:], 'Y']   # 嵌入一個L的副本

>>> L[1] = 0               # 僅僅改變了L,但是不影響M

[1, 0, 3]

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

靜态識别本地域的變量名

Python預設将一個函數中指派的變量名視作是本地域的,它們存在于該函數的作用域中并且僅僅在函數運作的時候才存在。從技術上講,Python是在編譯def代碼時,去靜态的識别本地變量,而不是在運作時碰到指派的時候才識别到的。如果不了解這點的話,會引起人們的誤解。比如,看看下面的例子,當你在一個引用之後給一個變量指派會怎麼樣:

>>> X = 99

>>> def func():

...    print X      # 這個時候還不存在

...    X = 88       # 在整個def中将X視作本地變量

...

>>> func( )          # 出錯了!

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

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

預設參數和可變對象

在執行def語句時,預設參數的值隻被解析并儲存一次,而不是每次在調用函數的時候。這通常是你想要的那樣,但是因為預設值需要在每次調用時都保持同樣對象,你在試圖改變可變的預設值(mutable defaults)的時候可要小心了。例如,下面的函數中使用一個空的清單作為預設值,然後在之後每一次函數調用的時候改變它的值:

>>> def saver(x=[]):   # 儲存一個清單對象

...    x.append(1)    # 并每次調用的時候

...    print x     # 改變它的值

>>> saver([2])         # 未使用預設值

[2, 1]

>>> saver()            # 使用預設值

[1]

>>> saver()            # 每次調用都會增加!

[1, 1]

>>> saver()

[1, 1, 1]

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

要擺脫這樣的行為,在函數開始的地方用切片或者方法來建立預設參數的副本,或者将預設值的表達式移到函數裡面;隻要每次函數調用時這些值在函數裡,就會每次都得到一個新的對象:

>>> def saver(x=None):

...    if x is None: x = []   # 沒有傳入參數?

...    x.append(1)            # 改變新的清單

...    print x

>>> saver([2])                 # 沒有使用預設值

>>> saver()                    # 這次不會變了

其他常見的程式設計陷阱

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

  • 在頂層檔案中語句的順序是有講究的:因為運作或者加載一個檔案會從上到下運作它的語句,是以請確定将你未嵌套的函數調用或者類的調用放在函數或者類的定義之後。
  • 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社群。

繼續閱讀