這篇文章收集了我在Python新手開發者寫的程式碼中所見到的不規範但偶爾又很微妙的問題。本文的目的是為了幫助那些新手開發者渡過寫出醜陋的Python程式碼的階段。為了照顧目標讀者,本文做了一些簡化(例如:在討論迭代器的時候忽略了生成器和強大的迭代工具itertools)。
對於那些新手開發者,總是有一些使用反模式的理由,我已經嘗試在可能的地方給出了這些理由。但通常這些反模式會造成程式碼缺乏可讀性、更容易出bug且不符合Python的程式碼風格。如果你想尋找更多相關的介紹資料,我極力推薦The Python Tutorial或Dive into Python。
迭代
range的使用
Python編程新手喜歡使用range來實現簡單的迭代,在迭代器的長度範圍內來獲取迭代器中的每一個元素:
for i in range(len(alist)): print alist[i]
應該牢記range並不是為了實現序列簡單的迭代。相較於那些用數字定義的for循環,雖然用range實現的for循環顯得很自然,但是用在序列的迭代上卻容易出bug,而且不如直接構造迭代器看上去清晰:
for item in alist: print item
range的濫用容易造成意外的大小差一(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, value
2. 需要同時迭代兩個循環,用同一個索引來取得兩個值。在這種情況下,可以用zip來實現:
for word, number in zip(words, numbers): print word, number
3. 需要迭代序列的一部分。在這種情況下,只需要迭代序列切片就可以實現,注意添加必要的註釋註明用意:
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或set/dict中是否包含某個元素表面上看起來沒什麼區別,但是表面之下卻是截然不同的。如果你需要重複檢查某個資料結構裡是否包含某個元素,最好使用set來取代list。 (如果你想把一個值和要檢查的元素聯繫起來,可以使用dict;這樣同樣可以實現常數檢查時間。)
# 假设以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,因為你只需要創建一次。
變數洩漏
循環
通常說來,在Python中,一個變數的作用域比你在其他語言裡期望的要寬。例如:Java中下面的程式碼將不能透過編譯:
// 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])
外作用域
Python程序员新手经常喜欢把所有东西放到所谓的外作用域——python文件中不被代码块(例如函数或者类)包含的部分。外作用域相当于全局命名空间;为了这部分的讨论,你应该假设全局作用域的内容在单个Python文件的任何地方都是可以访问的。
对于定义整个模块都需要去访问的在文件顶部声明的常量,外作用域显得非常强大。给外作用域中的任何变量使用有特色的名字是明智的做法,例如,使用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(http://stackoverflow.com/questions/4693120/use-of-global-keyword-in-python/4693170#4693170)。
代码风格
向PEP8致敬
PEP 8是Python代码的通用风格指南,你应该牢记在心并且尽可能去遵循它,尽管一些人有充分的理由不同意其中一些细小的风格,例如缩进的空格个数或使用空行。如果你不遵循PEP8,你应该有除“我只是不喜欢那样的风格”之外更好的理由。下边的风格指南都是从PEP8中摘取的,似乎是编程者经常需要牢记的。
测试是否为空
如果你要检查一个容器类型(例如:列表,词典,集合)是否为空,只需要简单测试它而不是使用类似检查len(x)>0这样的方法:
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可以作为一个很好的哨兵值。那么如何检查它呢?
如果你明确的想要测试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] 在外作用於中不要給函數中的局部變數名稱設定任何值,以防止函數內部呼叫局部變數時發生錯誤而呼叫外部作用域中的同名變數。
以上就是Python 程式設計中的反模式 的內容,更多相關內容請關注PHP中文網(www.php.cn)!