Python編程中的反模式
Linux編程
點擊右側關注,免費入門到精通!
作者丨伯樂在線
http://python.jobbole.com/74252/
這篇文章收集了我在Python新手開發者寫的代碼中所見到的不規範但偶爾又很微妙的問題。
本文的目的是為了幫助那些新手開發者渡過寫出醜陋的Python代碼的階段。
對於那些新手開發者,總有一些使用反模式的理由,我已經嘗試在可能的地方給出了這些理由。
但通常這些反模式會造成代碼缺乏可讀性、更容易出bug且不符合Python的代碼風格。
迭代
range的使用
Python編程新手喜歡使用range來實現簡單的迭代,在迭代器的長度範圍內來獲取迭代器中的每一個元素:
應該牢記:range並不是為了實現序列簡單的迭代。相比那些用數字定義的for循環,雖然用range實現的for循環顯得很自然,但是用在序列的迭代上卻容易出bug,而且不如直接構造迭代器看上去清晰:
range的濫用容易造成意外的大小差一(off-by-one)錯誤,這通常是由於編程新手忘記了range生成的對象包括range的第一個參數而不包括第二個,類似於java中的substring和其他眾多這種類型的函數。那些認為沒有超出序列結尾的編程新手將會製造出bug:
不恰當地使用range的常見理由:
1.需要在循環中使用索引。
這並不是一個合理的理由,可以用以下方式代替使用索引:
2.需要同時迭代兩個循環,用同一個索引來獲取兩個值。
這種情況下,可以用zip來實現:
3.需要迭代序列的一部分。在這種情況下,僅需要迭代序列切片就可以實現,注意添加必要的注釋註明用意:
有一個例外:
當你迭代一個很大的序列時,切片操作引起的開銷就比較大。
如果序列只有10個元素,就沒有什麼問題;但是如果有1000萬個元素時,或者在一個性能敏感的內循環中進行切片操作時,開銷就變得非常重要了。
這種情況下可以考慮使用xrange代替range [1]。
在用來迭代序列之外,range的一個重要用法是當你真正想要生成一個數字序列而不是用來生成索引:
正確使用列表解析
如果你有像這樣的一個循環:
你可以使用列表解析來重寫:
為什麼要這麼做?
一方面你避免了正確初始化列表可能帶來的錯誤,另一方面,這樣寫代碼讓看起來很乾凈,整潔。
對於那些有函數式編程背景的人來說,使用map函數可能感覺更熟悉,但是在我看來這種做法不太Python化。
其他的一些不使用列表解析的常見理由:
1. 需要循環嵌套。
這個時候你可以嵌套整個列表解析,或者在列表解析中多行使用循環:
使用列表解析:
注意:在有多個循環的列表解析中,循環有同樣的順序就像你並沒有使用列表解析一樣。
2. 你在循環內部需要一個條件判斷。
你只需要把這個條件判斷添加到列表解析中去:
一個不使用列表解析的合理的理由是你在列表解析里不能使用異常處理。
如果迭代中一些元素可能引起異常,你需要在列表解析中通過函數調用轉移可能的異常處理,或者乾脆不使用列表解析。
性能缺陷
在線性時間內檢查內容
在語法上,檢查list或者set/dict中是否包含某個元素表面上看起來沒什麼區別,但是表面之下卻是截然不同的。
如果你需要重複檢查某個數據結構里是否包含某個元素,最好使用set來代替list。
(如果你想把一個值和要檢查的元素聯繫起來,可以使用dict;這樣同樣可以實現常數檢查時間。)
Python中set的元素和dict的鍵值是可哈希的,因此查找起來時間複雜度為O(1)。
應該記住:
創建set引入的是一次性開銷,創建過程將花費線性時間即使成員檢查花費常數時間。
因此如果你需要在循環里檢查成員,最好先花時間創建set,因為你只需要創建一次。
變數泄露
循環
通常說來,在Python中,一個變數的作用域比你在其他語言里期望的要寬。
例如:在Java中下面的代碼將不能通過編譯:
然而在Python中,同樣的代碼總會順利執行且得到意料中的結果:
這段代碼將會正常運行,除非子y為空的情況下,此時,循環永遠不會執行,而且processList函數的調用將會拋出NameError異常,因為idx沒有定義。
如果你使用Pylint代碼檢查工具,將會警告:使用可能沒有定義的變數idx。
解決辦法永遠是顯然的,可以在循環之前設置idx為一些特殊的值,這樣你就知道如果循環永遠沒有執行的時候你將要尋找什麼。
這種模式叫做哨兵模式。那麼什麼值可以用來作為哨兵呢?
在C語言時代或者更早,當int統治編程世界的時候,對於需要返回一個期望的錯誤結果的函數來說為通用的模式為返回-1。
例如,當你想要返回列表中某一元素的索引值:
通常情況下,在Python里None是一個比較好的哨兵值,即使它不是一貫地被Python標準類型使用(例如:str.find [2])
外作用域
Python程序員新手經常喜歡把所有東西放到所謂的外作用域——python文件中不被代碼塊(例如函數或者類)包含的部分。
外作用域相當於全局命名空間;為了這部分的討論,你應該假設全局作用域的內容在單個Python文件的任何地方都是可以訪問的。
對於定義整個模塊都需要去訪問的在文件頂部聲明的常量,外作用域顯得非常強大。
給外作用域中的任何變數使用有特色的名字是明智的做法,例如,使用IN_ALL_CAPS 這個常量名。 這將不容易造成如下bug:
如果你看的近一點,你將看到print_file函數的定義中用filenam命名參數名,但是函數體卻引用的卻是filename。
然而,這個程序仍然可以運行得很好。
為什麼呢?
在print_file函數里,當一個局部變數filename沒有被找到時,下一步是在全局作用域中去尋找。
由於print_file的調用在外作用域中(即使有縮進),這裡聲明的filename對於print_file函數是可見的。
那麼如何避免這樣的錯誤呢?
首先,在外作用域中不是IN_ALL_CAPS這樣的全局變數就不要設置任何值[3]。
參數解析最好交給main函數,因此函數中任何內部變數不在外作用域中存活。
這也提醒人們關注全局關鍵字global。如果你只是讀取全局變數的值,你就不需要全局關鍵字global。
你只有在想要改變全局變數名引用的對象時有使用global關鍵字的必要。
代碼風格
向PEP8致敬
PEP 8是Python代碼的通用風格指南,你應該牢記在心並且儘可能去遵循它,儘管一些人有充分的理由不同意其中一些細小的風格,例如縮進的空格個數或使用空行。
如果你不遵循PEP8,你應該有除「我只是不喜歡那樣的風格」之外更好的理由。下邊的風格指南都是從PEP8中摘取的,似乎是編程者經常需要牢記的。
測試是否為空
如果你要檢查一個容器類型(例如:列表,詞典,集合)是否為空,只需要簡單測試它而不是使用類似檢查len(x)>0這樣的方法:
如果你想在其他地方保存positive_numbers是否為空的結果,可以使用bool(positive_number)作為結果保存;bool用來判斷if條件判斷語句的真值。
測試是否為None
如前面所提到,None可以作為一個很好的哨兵值。那麼如何檢查它呢?
如果你明確的想要測試None,而不只是測試其他一些值為False的項(如空容器或者0),可以使用:
如果你使用None作為哨兵,這也是Python風格所期望的模式,例如在你想要區分None和0的時候。
如果你只是測試變數是否為一些有用的值,一個簡單的if模式通常就夠用了:
例如:如果期望x是一個容器類型,但是x可能作另一個函數的返回結果值變為None,你應該立即考慮到這種情況。你需要留意是否改變了傳給x的值,否則可能你認為True或0. 0是個有用的值,程序卻不會按照你想要的方式執行。
推薦↓↓↓
長
按
關
注
??
【
16個技術公眾號
】都在這裡!
涵蓋:程序員大咖、源碼共讀、程序員共讀、數據結構與演算法、黑客技術和網路安全、大數據科技、編程前端、Java、Python、Web編程開發、Android、iOS開發、Linux、資料庫研發、幽默程序員等。
萬水千山總是情,點個 「
好看
」 行不行TAG:Python開發 |