Python程序员的30个常见错误
全世界只有3.14 %?的人關注了
數據與算法之美
在這篇文章中,我將總結新老Python程序員常犯的一些錯誤,以幫助你們在自己的工作避免犯同樣或類似錯誤。推薦閱讀《Python3.0科學計算指南》
首先我要說明一下的是,這些都是來源于第一手的經驗。我以講授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系列叢書的《Python學習手冊》(原書第5版)里有詳細的解讀。
01 語用錯誤
讓我們從基礎開始,從那些剛學習編程的人鉆研語法之前碰到的事情開始。如果你已經編過一些程了,那么以下這些可能看起來十分的簡單;如果你曾經嘗試過教新手們怎么編程,它們可能就不這么簡單了。
1. 在交互提示符中輸入Python代碼
在>>>交互提示符中你只能輸入Python代碼,而不是系統命令。時常有人在這個提示符下輸入emacs,ls,或者edit之類的命令,這些可不是Python代碼。
在Python代碼中確實有辦法來調用系統命令(例如os.system和os.popen),但可不是像直接輸入命令這么直接。如果你想要在交互提示符中啟動一個Python文件,請用import file,而不是系統命令python file.py。
2. Print語句(僅僅)是在文件中需要
因為交互解釋器會自動的講表達式的結果輸出,所以你不需要交互的鍵入完整的print語句。這是個很棒的功能,但是記住在代碼文件里,通常你只有用print語句才能看得到輸出。
3. 小心Windows里的自動擴展名
如果你在Windows里使用記事本來編輯代碼文件的話,當你保持的時候小心選擇“所有文件”(All Files)這個類型,并且明確的給你的文件加一個.py的后綴。不然的話記事本會給你的文件加一個.txt的擴展名,使得在某些啟動方法中沒法跑這個程序。
更糟糕的是,像Word或者是寫字板一類的文字處理軟件還會默認的加上一些格式字符,而這些字符Python語法是不認的。
所以記得,在Windows下總是選“所有文件”(All Files),并保存為純文本,或者使用更加“編程友好”的文本編輯工具,比如IDLE。在IDLE中,記得在保存時手動加上.py的擴展名。
4. 在Windows下點擊圖標的問題
在Windows下,你能靠點擊Python文件來啟動一個Python程序,但這有時會有問題。首先,程序的輸出窗口在程序結束的瞬間也就消失了,要讓它不消失,你可以在文件最后加一條raw_input()的調用。另外,記住如果有錯的話,輸出窗口也就立即消失了。
要看到你的錯誤信息的話,用別的方法來調用你的程序:比如從系統命令行啟動,通過提示符下用import語句,或者IDLE菜單里的選項,等等。
5. Import只在第一次有效
你可以在交互提示符中通過import一個文件來運行它,但是這只會在一個會話中起一次作用;接下來的import僅僅是返回這個已經加載的模塊。要想強制Python重新加載一個文件的代碼,請調用函數reload(module)來達到這個目的。注意對reload請使用括號,而import不要使用括號。
6. 空白行(僅僅)在交互提示符中有作用
在模塊文件中空白行和注釋統統會被忽略掉,但是在交互提示符中鍵入代碼時,空白行表示一個復合語句的結束。
換句話說,空白行告訴交互提示符你完成了一個復合語句;在你真正完成之前不要鍵入回車。事實上當你要開始一個新的語句時,你需要鍵入一個空行來結束當前的語句——交互提示符一次只運行一條語句。
02 代碼錯誤
一旦你開始認真寫Python代碼了,接下來了一堆陷阱就更加危險了——這些都是一些跨語言特性的基本代碼錯誤,并常常困擾不細心的程序員。
7. 別忘了冒號
這是新手程序員最容易犯的一個錯誤:別忘了在復合語句的起始語句(if,while, for等語句的第一行)結束的地方加上一個冒號“:”。也許你剛開始會忘掉這個,但是到了很快這就會成為一個下意識的習慣。課堂里75%的學生當天就可以記住這個。
8. 初始化變量
在Python里,一個表達式中的名字在它被賦值之前是沒法使用的。這是有意而為的:這樣能避免一些輸入失誤,同時也能避免默認究竟應該是什么類型的問題(0,None,””,[],?)。記住把計數器初始化為0,列表初始化為[],以此類推。
9. 從第一列開始
確保把頂層的,未嵌套的代碼放在最左邊第一列開始。這包括在模塊文件中未嵌套的代碼,以及在交互提示符中未嵌套的代碼。Python使用縮進的辦法來區分嵌套的代碼段,因此在你代碼左邊的空格意味著嵌套的代碼塊。除了縮進以外,空格通常是被忽略掉的。
10. 縮進一致
在同一個代碼塊中避免講tab和空格混用來縮進,除非你知道運行你的代碼的系統是怎么處理tab的。否則的話,在你的編輯器里看起來是tab的縮進也許Python看起來就會被視作是一些空格。保險起見,在每個代碼塊中全都是用tab或者全都是用空格來縮進;用多少由你決定。
11. 在函數調用時使用括號
無論一個函數是否需要參數,你必須要加一對括號來調用它。即,使用function(),而不是function。Python的函數簡單來說是具有特殊功能(調用)的對象,而調用是用括號來觸發的。像所有的對象一樣,他們也可以被賦值給變量,并且間接的使用他們:x=function:x()。
在Python的培訓中,這樣的錯誤常常在文件的操作中出現。通常會看到新手用file.close來關閉一個問題,而不是用file.close()。因為在Python中引用一個函數而不調用它是合法的,因此不使用括號的操作(file.close)無聲的成功了,但是并沒有關閉這個文件!
12. 在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的模塊,最后可能什么也找不到而報出一系列費解的錯誤信息。
13. 不要在Python中寫C代碼
以下是給不熟悉Python的C程序員的一些備忘貼士:
在if和while中條件測試時,不用輸入括號(例如,if (X==1):)。如果你喜歡的話,加上括號也無妨,只是在這里是完全多余的。
不要用分號來結束你的語句。從技術上講這在Python里是合法的,但是這毫無用處,除非你要把很多語句放在同一行里(例如,x=1; y=2; z=3)。
不要在while循環的條件測試中嵌入賦值語句(例如,while ((x=next() != NULL))。在Python中,需要表達式的地方不能出現語句,并且賦值語句不是一個表達式。
03 編程錯誤
下面終于要講到當你用到更多的Python的功能(數據類型,函數,模塊,類等等)時可能碰到的問題了。由于篇幅有限,這里盡量精簡,尤其是對一些高級的概念。要想了解更多的細節,敬請閱讀《Python學習手冊》。
14. 打開文件的調用不使用模塊搜索路徑
當你在Python中調用open()來訪問一個外部的文件時,Python不會使用模塊搜索路徑來定位這個目標文件。它會使用你提供的絕對路徑,或者假定這個文件是在當前工作目錄中。模塊搜索路徑僅僅為模塊加載服務的。
15. 不同的類型對應的方法也不同
列表的方法是不能用在字符串上的,反之亦然。通常情況下,方法的調用是和數據類型有關的,但是內部函數通常在很多類型上都可以使用。舉個例子來說,列表的reverse方法僅僅對列表有用,但是len函數對任何具有長度的對象都適用。
16. 不能直接改變不可變數據類型
記住你沒法直接的改變一個不可變的對象(例如,元組,字符串):
T[2]?=?4??????????#?錯誤
用切片,聯接等構建一個新的對象,并根據需求將原來變量的值賦給它。因為Python會自動回收沒有用的內存,因此這沒有看起來那么浪費:
17. 使用簡單的for循環而不是while或者range
當你要從左到右遍歷一個有序的對象的所有元素時,用簡單的for循環(例如,for x in seq:)相比于基于while-或者range-的計數循環而言會更容易寫,通常運行起來也更快。
除非你一定需要,盡量避免在一個for循環里使用range:讓Python來替你解決標號的問題。在下面的例子中三個循環結構都沒有問題,但是第一個通常來說更好;在Python里,簡單至上。
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
18. 不要試圖從那些會改變對象的函數得到結果
諸如像方法list.append()和list.sort()一類的直接改變操作會改變一個對象,但不會將它們改變的對象返回出來(它們會返回None);正確的做法是直接調用它們而不要將結果賦值。經常會看見初學者會寫諸如此類的代碼:
目的是要得到append的結果,但是事實上這樣做會將None賦值給mylist,而不是改變后的列表。更加特別的一個例子是想通過用排序后的鍵值來遍歷一個字典里的各個元素,請看下面的例子:
for?k?in?D.keys().sort():?print?D[k]
差一點兒就成功了——keys方法會創建一個keys的列表,然后用sort方法來將這個列表排序——但是因為sort方法會返回None,這個循環會失敗,因為它實際上是要遍歷None(這可不是一個序列)。要改正這段代碼,將方法的調用分離出來,放在不同的語句中,如下:
Ks.sort()
for?k?in?Ks:?print?D[k]
19. 只有在數字類型中才存在類型轉換
在Python中,一個諸如123+3.145的表達式是可以工作的——它會自動將整數型轉換為浮點型,然后用浮點運算。但是下面的代碼就會出錯了:
I?=?1
X?=?S?+?I????????#?類型錯誤
這同樣也是有意而為的,因為這是不明確的:究竟是將字符串轉換為數字(進行相加)呢,還是將數字轉換為字符串(進行聯接)呢?在Python中,我們認為“明確比含糊好”(即,EIBTI(Explicit is better than implicit)),因此你得手動轉換類型:
X?=?S?+?str(I)???#?字符串聯接:?"421"
20. 循環的數據結構會導致循環
盡管這在實際情況中很少見,但是如果一個對象的集合包含了到它自己的引用,這被稱為循環對象(cyclic object)。如果在一個對象中發現一個循環,Python會輸出一個[…],以避免在無限循環中卡住:
>>>?L.append(L)????#?在對象中創造一個循環
>>>?L
['grail',?[...]]
除了知道這三個點在對象中表示循環以外,這個例子也是很值得借鑒的。因為你可能無意間在你的代碼中出現這樣的循環的結構而導致你的代碼出錯。如果有必要的話,維護一個列表或者字典來表示已經訪問過的對象,然后通過檢查它來確認你是否碰到了循環。
21. 賦值語句不會創建對象的副本,僅僅創建引用
這是Python的一個核心理念,有時候當行為不對時會帶來錯誤。在下面的例子中,一個列表對象被賦給了名為L的變量,然后L又在列表M中被引用。內部改變L的話,同時也會改變M所引用的對象,因為它們倆都指向同一個對象。
>>>?M?=?['X',?L,?'Y']????#?嵌入一個到L的引用
>>>?M
['X',?[1,?2,?3],?'Y']
>>>?L[1]?=?0?????????????#?也改變了M
>>>?M
['X',?[1,?0,?3],?'Y']
通常情況下只有在稍大一點的程序里這就顯得很重要了,而且這些共用的引用通常確實是你需要的。如果不是的話,你可以明確的給他們創建一個副本來避免共用的引用;對于列表來說,你可以通過使用一個空列表的切片來創建一個頂層的副本:
>>>?M?=?['X',?L[:],?'Y']???#?嵌入一個L的副本
>>>?L[1]?=?0???????????????#?僅僅改變了L,但是不影響M
>>>?L
[1,?0,?3]
>>>?M
['X',?[1,?2,?3],?'Y']
切片的范圍起始從默認的0到被切片的序列的最大長度。如果兩者都省略掉了,那么切片會抽取該序列中的所有元素,并創造一個頂層的副本(一個新的,不被公用的對象)。對于字典來說,使用字典的dict.copy()方法。
22. 靜態識別本地域的變量名
Python默認將一個函數中賦值的變量名視作是本地域的,它們存在于該函數的作用域中并且僅僅在函數運行的時候才存在。從技術上講,Python是在編譯def代碼時,去靜態的識別本地變量,而不是在運行時碰到賦值的時候才識別到的。
如果不理解這點的話,會引起人們的誤解。比如,看看下面的例子,當你在一個引用之后給一個變量賦值會怎么樣:
>>>?def?func():
...?????print?X??????#?這個時候還不存在
...?????X?=?88???????#?在整個def中將X視作本地變量
...
>>>?func(?)??????????#?出錯了!
你會得到一個“未定義變量名”的錯誤,但是其原因是很微妙的。當編譯這則代碼時,Python碰到給X賦值的語句時認為在這個函數中的任何地方X會被視作一個本地變量名。
但是之后當真正運行這個函數時,執行print語句的時候,賦值語句還沒有發生,這樣Python便會報告一個“未定義變量名”的錯誤。
事實上,之前的這個例子想要做的事情是很模糊的:你是想要先輸出那個全局的X,然后創建一個本地的X呢,還是說這是個程序的錯誤?如果你真的是想要輸出這個全局的X,你需要將它在一個全局語句中聲明它,或者通過包絡模塊的名字來引用它。
23. 默認參數和可變對象
在執行def語句時,默認參數的值只被解析并保存一次,而不是每次在調用函數的時候。這通常是你想要的那樣,但是因為默認值需要在每次調用時都保持同樣對象,你在試圖改變可變的默認值(mutable defaults)的時候可要小心了。
例如,下面的函數中使用一個空的列表作為默認值,然后在之后每一次函數調用的時候改變它的值:
...?????x.append(1)????#?并每次調用的時候
...?????print?x????#?改變它的值
...
>>>?saver([2])?????????#?未使用默認值
[2,?1]
>>>?saver()????????????#?使用默認值
[1]
>>>?saver()????????????#?每次調用都會增加!
[1,?1]
>>>?saver()
[1,?1,?1]
有的人將這個視作Python的一個特點——因為可變的默認參數在每次函數調用時保持了它們的狀態,它們能提供像C語言中靜態本地函數變量的類似的一些功能。
但是,當你第一次碰到它時會覺得這很奇怪,并且在Python中有更加簡單的辦法來在不同的調用之間保存狀態(比如說類)。
要擺脫這樣的行為,在函數開始的地方用切片或者方法來創建默認參數的副本,或者將默認值的表達式移到函數里面;只要每次函數調用時這些值在函數里,就會每次都得到一個新的對象:
...?????if?x?is?None:?x?=?[]???#?沒有傳入參數?
...?????x.append(1)????????????#?改變新的列表
...?????print?x
...
>>>?saver([2])?????????????????#?沒有使用默認值
[2,?1]
>>>?saver()????????????????????#?這次不會變了
[1]
>>>?saver()
[1]
24. 其他常見的編程陷阱
下面列舉了其他的一些在這里沒法詳述的陷阱:
在頂層文件中語句的順序是有講究的:因為運行或者加載一個文件會從上到下運行它的語句,所以請確保將你未嵌套的函數調用或者類的調用放在函數或者類的定義之后。
reload不影響用from加載的名字:reload最好和import語句一起使用。如果你使用from語句,記得在reload之后重新運行一遍from,否則你仍然使用之前老的名字。
在多重繼承中混合的順序是有講究的:這是因為對superclass的搜索是從左到右的,在類定義的頭部,在多重superclass中如果出現重復的名字,則以最左邊的類名為準。
在try語句中空的except子句可能會比你預想的捕捉到更多的錯誤。在try語句中空的except子句表示捕捉所有的錯誤,即便是真正的程序錯誤,和sys.exit()調用,也會被捕捉到。
兔子可能會比他們看起來更加危險。
作者:Mark Lutz?
來源:http://blog.jobbole.com/69834/
版權歸原作者所有,轉載僅供學習使用,不用于任何商業用途,如有侵權請留言聯系刪除,感謝合作。
精品課程推薦:
選購數學科普正版讀物
嚴選“數學思維好物”
送給孩子的益智禮物? ?| ??辦公室神器
算法工程師成長閱讀? ?| ??居家高科技
理工科男女實用型禮物精選? ?
----點擊頭像關注----
超級數學建模
數據與算法之美
少年數學家
數銳學堂
驚喜醬(個人號)
玩酷屋COOL
總結
以上是生活随笔為你收集整理的Python程序员的30个常见错误的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 这里聚集了优秀的数学老师、家长,有超多惊
- 下一篇: 2018 年最受欢迎的 Python 库