迭代
- range的使用
for i in range(len(alist)): print alist[i]應該牢記:range並不是為了實現序列簡單的迭代。相比那些用數字定義的for迴圈,雖然用range實現的for迴圈顯得很自然,但是用在序列的迭代上卻容易出bug,而且不如直接構造迭代器看上去清晰:
for item in alist: print itemrange的濫用容易造成意外的大小差一(off-by-one)錯誤,這通常是由於程式設計新手忘記了range生成的物件包括range的第一個引數而不包括第二個,類似於java中的substring和其他眾多這種型別的函式。那些認為沒有超出序列結尾的程式設計新手將會製造出bug:
# 迭代整個序列錯誤的方法 alist = ['her', 'name', 'is', 'rio'] for i in range(0, len(alist) - 1): # 大小差一(Off by one)! print i, alist[i]不恰當地使用range的常見理由:
1. 需要在迴圈中使用索引。這並不是一個合理的理由,可以用以下方式代替使用索引:
for index, value in enumerate(alist): print index, value2. 需要同時迭代兩個迴圈,用同一個索引來獲取兩個值。這種情況下,可以用zip來實現:
for word, number in zip(words, numbers): print word, number3. 需要迭代序列的一部分。在這種情況下,僅需要迭代序列切片就可以實現,注意新增必要的註釋註明用意:
for word in words[1:]: # 不包括第一個元素 print word有一個例外:當你迭代一個很大的序列時,切片操作引起的開銷就比較大。如果序列只有10個元素,就沒有什麼問題;但是如果有1000萬個元素時,或者在一個效能敏感的內迴圈中進行切片操作時,開銷就變得非常重要了。這種情況下可以考慮使用xrange代替range [1]。
在用來迭代序列之外,range的一個重要用法是當你真正想要生成一個數字序列而不是用來生成索引:
# Print foo(x) for 0<=x<5 for x in range(5): print foo(x)
- 正確使用列表解析
# An ugly, slow way to build a list words = ['her', 'name', 'is', 'rio'] alist = [] for word in words: alist.append(foo(word))你可以使用列表解析來重寫:
words = ['her', 'name', 'is', 'rio'] alist = [foo(word) for word in words]為什麼要這麼做?一方面你避免了正確初始化列表可能帶來的錯誤,另一方面,這樣寫程式碼讓看起來很乾淨,整潔。對於那些有函數語言程式設計背景的人來說,使用map函式可能感覺更熟悉,但是在我看來這種做法不太Python化。
其他的一些不使用列表解析的常見理由:
1. 需要迴圈巢狀。這個時候你可以巢狀整個列表解析,或者在列表解析中多行使用迴圈:
words = ['her', 'name', 'is', 'rio'] letters = [] for word in words: for letter in word: letters.append(letter)使用列表解析:
words = ['her', 'name', 'is', 'rio'] letters = [letter for word in words for letter in word]注意:在有多個迴圈的列表解析中,迴圈有同樣的順序就像你並沒有使用列表解析一樣。
2. 你在迴圈內部需要一個條件判斷。你只需要把這個條件判斷新增到列表解析中去:
words = ['her', 'name', 'is', 'rio', '1', '2', '3'] alpha_words = [word for word in words if isalpha(word)]一個不使用列表解析的合理的理由是你在列表解析裡不能使用異常處理。如果迭代中一些元素可能引起異常,你需要在列表解析中通過函式呼叫轉移可能的異常處理,或者乾脆不使用列表解析。
效能缺陷
- 線上性時間內檢查內容
# 假設以list開始 lyrics_list = ['her', 'name', 'is', 'rio'] # 避免下面的寫法 words = make_wordlist() # 假設返回許多要測試的單詞 for word in words: if word in lyrics_list: # 線性檢查時間 print word, "is in the lyrics" # 最好這麼寫 lyrics_set = set(lyrics_list) # 線性時間建立set words = make_wordlist() # 假設返回許多要測試的單詞 for word in words: if word in lyrics_set: # 常數檢查時間 print word, "is in the lyrics"[譯者注:Python中set的元素和dict的鍵值是可雜湊的,因此查詢起來時間複雜度為O(1)。]
應該記住:建立set引入的是一次性開銷,建立過程將花費線性時間即使成員檢查花費常數時間。因此如果你需要在迴圈裡檢查成員,最好先花時間建立set,因為你只需要建立一次。
變數洩露
- 迴圈
// Get the index of the lowest-indexed item in the array // that is > maxValue for(int i = 0; i < y.length; i++) { if (y[i] > maxValue) { break; } } // i在這裡出現不合法:不存在i processArray(y, i);然而在Python中,同樣的程式碼總會順利執行且得到意料中的結果:
for idx, value in enumerate(y): if value > max_value: break processList(y, idx)這段程式碼將會正常執行,除非子y為空的情況下,此時,迴圈永遠不會執行,而且processList函式的呼叫將會丟擲NameError異常,因為idx沒有定義。如果你使用Pylint程式碼檢查工具,將會警告:使用可能沒有定義的變數idx。
解決辦法永遠是顯然的,可以在迴圈之前設定idx為一些特殊的值,這樣你就知道如果迴圈永遠沒有執行的時候你將要尋找什麼。這種模式叫做哨兵模式。那麼什麼值可以用來作為哨兵呢?在C語言時代或者更早,當int統治程式設計世界的時候,對於需要返回一個期望的錯誤結果的函式來說為通用的模式為返回-1。例如,當你想要返回列表中某一元素的索引值:
def find_item(item, alist): # None比-1更加Python化 result = -1 for idx, other_item in enumerate(alist): if other_item == item: result = idx break return result通常情況下,在Python裡None是一個比較好的哨兵值,即使它不是一貫地被Python標準型別使用(例如:str.find [2])
- 外作用域
對於定義整個模組都需要去訪問的在檔案頂部宣告的常量,外作用域顯得非常強大。給外作用域中的任何變數使用有特色的名字是明智的做法,例如,使用IN_ALL_CAPS 這個常量名。 這將不容易造成如下bug:
import sys # See the bug in the function declaration? def print_file(filenam): """Print every line of a file.""" with open(filename) as input_file: for line in input_file: print line.strip() if __name__ == "__main__": filename = sys.argv[1] print_file(filename)如果你看的近一點,你將看到print_file函式的定義中用filenam命名引數名,但是函式體卻引用的卻是filename。然而,這個程式仍然可以執行得很好。為什麼呢?在print_file函式裡,當一個區域性變數filename沒有被找到時,下一步是在全域性作用域中去尋找。由於print_file的呼叫在外作用域中(即使有縮排),這裡宣告的filename對於print_file函式是可見的。
那麼如何避免這樣的錯誤呢?首先,在外作用域中不是IN_ALL_CAPS這樣的全域性變數就不要設定任何值[3]。引數解析最好交給main函式,因此函式中任何內部變數不在外作用域中存活。
這也提醒人們關注全域性關鍵字global。如果你只是讀取全域性變數的值,你就不需要全域性關鍵字global。你只有在想要改變全域性變數名引用的物件時有使用global關鍵字的必要。你可以在這裡獲取更多相關資訊this discussion of the global keyword on Stack Overflow。
程式碼風格
- 向PEP8致敬
- 測試是否為空
numbers = [-1, -2, -3] # This will be empty positive_numbers = [num for num in numbers if num > 0] if positive_numbers: # Do something awesome如果你想在其他地方儲存positive_numbers是否為空的結果,可以使用bool(positive_number)作為結果儲存;bool用來判斷if條件判斷語句的真值。
- 測試是否為None
如果你明確的想要測試None,而不只是測試其他一些值為False的項(如空容器或者0),可以使用:
if x is not None: # Do something with x如果你使用None作為哨兵,這也是Python風格所期望的模式,例如在你想要區分None和0的時候。
如果你只是測試變數是否為一些有用的值,一個簡單的if模式通常就夠用了:
if x: # Do something with x例如:如果期望x是一個容器型別,但是x可能作另一個函式的返回結果值變為None,你應該立即考慮到這種情況。你需要留意是否改變了傳給x的值,否則可能你認為True或0. 0是個有用的值,程式卻不會按照你想要的方式執行。
譯者注:
- [*][1] 在Python2.x 中 range生成的是list物件,xrange生成的則是range物件;Python 3.x 廢除了xrange,range生成的統一為range物件,用list工廠函式可以顯式生成list;[*][2] string.find(str)返回str在string中開始的索引值,如果不存在則返回-1;[*][3] 在外作用於中不要給函式中的區域性變數名設定任何值,以防止函式內部呼叫區域性變數時發生錯誤而呼叫外部作用域中的同名變數。
相關閱讀
評論(4)