Python编程中的反模式(1)


这篇文章收集了我在Python新手开发者写的代码中所见到的不规范但偶尔又很微妙的问题。本文的目的是为了帮助那些新手开发者渡过写出丑陋的Python代码的阶段。为了照顾目标读者,本文做了一些简化例如:在讨论迭代器的时候忽略了生成器和强大的迭代工具itertools)。

对于那些新手开发者,总有一些使用反模式的理由,我已经尝试在可能的地方给出了这些理由。但通常这些反模式会造成代码缺乏可读性、更容易出bug且不符合Python的代码风格。如果你想要寻找更多的相关介绍资料,我极力推荐The Python TutorialDive into Python

迭代

  • range的使用

Python编程新手喜欢使用range来实现简单的迭代,在迭代器的长度范围内来获取迭代器中的每一个元素:

  1. for i in range(len(alist)):  
  2.     print alist[I] 

应该牢记:range并不是为了实现序列简单的迭代。相比那些用数字定义的for循环,虽然用range实现的for循环显得很自然,但是用在序列的迭代上却容易出bug,而且不如直接构造迭代器看上去清晰:

  1. for item in alist:  
  2.     print item 

range的滥用容易造成意外的大小差一(off-by-one)错误,这通常是由于编程新手忘记了range生成的对象包括range的第一个参数而不包括第二个,类似于java中的substring和其他众多这种类型的函数。那些认为没有超出序列结尾的编程新手将会制造出bug:

  1. # 迭代整个序列错误的方法  
  2. alist = ['her''name''is''rio']  
  3. for i in range(0, len(alist) - 1): # 大小差一Off by one)!  
  4.     print i, alist[I] 

不恰当地使用range的常见理由:
1. 需要在循环中使用索引。这并不是一个合理的理由,可以用以下方式代替使用索引:

  1. for index, value in enumerate(alist):  
  2.     print index, value 

2. 需要同时迭代两个循环,用同一个索引来获取两个值。这种情况下,可以用zip来实现:

  1. for word, number in zip(words, numbers):  
  2.     print word, number 

3. 需要迭代序列的一部分。在这种情况下,仅需要迭代序列切片就可以实现,注意添加必要的注释注明用意:

  1. for word in words[1:]: # 不包括第一个元素  
  2.     print word 

有一个例外:当你迭代一个很大的序列时,切片操作引起的开销就比较大。如果序列只有10个元素,就没有什么问题;但是如果有1000万个元素时,或者在一个性能敏感的内循环中进行切片操作时,开销就变得非常重要了。这种情况下可以考虑使用xrange代替range [1]。

在用来迭代序列之外,range的一个重要用法是当你真正想要生成一个数字序列而不是用来生成索引:

  1. # Print foo(x) for 0<=x<5  
  2. for x in range(5):  
  3.     print foo(x) 
  • 正确使用列表解析

如果你有像这样的一个循环:

  1. # An ugly, slow way to build a list  
  2. words = ['her''name''is''rio']  
  3. alist = []  
  4. for word in words:  
  5.     alist.append(foo(word)) 

你可以使用列表解析来重写:

  1. words = ['her''name''is''rio']  
  2. alist = [foo(word) for word in words] 

为什么要这么做?一方面你避免了正确初始化列表可能带来的错误,另一方面,这样写代码让看起来很干净,整洁。对于那些有函数式编程背景的人来说,使用map函数可能感觉更熟悉,但是在我看来这种做法不太Python化。

其他的一些不使用列表解析的常见理由:

1. 需要循环嵌套。这个时候你可以嵌套整个列表解析,或者在列表解析中多行使用循环:

  1. words = ['her''name''is''rio']  
  2. letters = []  
  3. for word in words:  
  4.     for letter in word:  
  5.         letters.append(letter) 

使用列表解析:

  1. words = ['her''name''is''rio']  
  2. letters = [letter for word in words  
  3.                   for letter in word] 

注意:在有多个循环的列表解析中,循环有同样的顺序就像你并没有使用列表解析一样。

2. 你在循环内部需要一个条件判断。你只需要把这个条件判断添加到列表解析中去:

  1. words = ['her''name''is''rio''1''2''3']  
  2. alpha_words = [word for word in words if isalpha(word)] 

一个不使用列表解析的合理的理由是你在列表解析里不能使用异常处理。如果迭代中一些元素可能引起异常,你需要在列表解析中通过函数调用转移可能的异常处理,或者干脆不使用列表解析。

性能缺陷

  • 在线性时间内检查内容

在语法上,检查list或者set/dict中是否包含某个元素表面上看起来没什么区别,但是表面之下却是截然不同的。如果你需要重复检查某个数据结构里是否包含某个元素,最好使用set来代替list。如果你想把一个值和要检查的元素联系起来,可以使用dict;这样同样可以实现常数检查时间。)

  1. # 假设以list开始  
  2. lyrics_list = ['her''name''is''rio']  
  3.    
  4. # 避免下面的写法  
  5. words = make_wordlist() # 假设返回许多要测试的单词  
  6. for word in words:  
  7.     if word in lyrics_list: # 线性检查时间  
  8.         print word, "is in the lyrics" 
  9.    
  10. # 最好这么写  
  11. lyrics_set = set(lyrics_list) # 线性时间创建set  
  12. words = make_wordlist() # 假设返回许多要测试的单词  
  13. for word in words:  
  14.     if word in lyrics_set: # 常数检查时间  
  15.         print word, "is in the lyrics" 

[译者注:Python中set的元素和dict的键值是可哈希的,因此查找起来时间复杂度为O(1)。]

应该记住:创建set引入的是一次性开销,创建过程将花费线性时间即使成员检查花费常数时间。因此如果你需要在循环里检查成员,最好先花时间创建set,因为你只需要创建一次。


评论关闭