4.3 风格的问题

编程是作为一门科学的艺术。无可争议的程序设计的“圣经”,Donald Knuth 的 2500 页的多卷作品,叫做《计算机程序设计艺术》。已经有许多书籍是关于文学化编程的,它们认为人类,不只是电脑,必须阅读和理解程序。在这里,我们挑选了一些编程风格的问题,它们对你的代码的可读性,包括代码布局、程序与声明的风格、使用循环变量都有重要的影响。

Python 代码风格

编写程序时,你会做许多微妙的选择:名称、间距、注释等等。当你在看别人编写的代码时,风格上的不必要的差异使其难以理解。因此,Python 语言的设计者发表了 Python 代码风格指南,httphttp://www.python.org/dev/peps/pep-0008/。风格指南中提出的基本价值是一致性,目的是最大限度地提高代码的可读性。我们在这里简要回顾一下它的一些主要建议,并请读者阅读完整的指南,里面有对实例的详细的讨论。

代码布局中每个缩进级别应使用 4 个空格。你应该确保当你在一个文件中写 Python 代码时,避免使用 tab 缩进,因为它可能由于不同的文本编辑器的不同解释而产生混乱。每行应少于 80 个字符长;如果必要的话,你可以在圆括号、方括号或花括号内换行,因为 Python 能够探测到该行与下一行是连续的。如果你需要在圆括号、方括号或大括号中换行,通常可以添加额外的括号,也可以在行尾需要换行的地方添加一个反斜杠:

  1. >>> if (len(syllables) > 4 and len(syllables[2]) == 3 and
  2. ... syllables[2][2] in [aeiou] and syllables[2][3] == syllables[1][3]):
  3. ... process(syllables)
  4. >>> if len(syllables) > 4 and len(syllables[2]) == 3 and \
  5. ... syllables[2][2] in [aeiou] and syllables[2][3] == syllables[1][3]:
  6. ... process(syllables)

注意

键入空格来代替制表符很快就会成为一件苦差事。许多程序编辑器内置对 Python 的支持,能自动缩进代码,突出任何语法错误(包括缩进错误)。关于 Python 编辑器列表,请见http://wiki.python.org/moin/PythonEditors

过程风格与声明风格

我们刚才已经看到可以不同的方式执行相同的任务,其中蕴含着对执行效率的影响。另一个影响程序开发的因素是 编程风格 。思考下面的计算布朗语料库中词的平均长度的程序:

  1. >>> tokens = nltk.corpus.brown.words(categories='news')
  2. >>> count = 0
  3. >>> total = 0
  4. >>> for token in tokens:
  5. ... count += 1
  6. ... total += len(token)
  7. >>> total / count
  8. 4.401545438271973

在这段程序中,我们使用变量count跟踪遇到的词符的数量,total储存所有词的长度的总和。这是一个低级别的风格,与机器代码,即计算机的 CPU 所执行的基本操作,相差不远。两个变量就像 CPU 的两个寄存器,积累许多中间环节产生的值,和直到最才有意义的值。我们说,这段程序是以 过程 风格编写,一步一步口授机器操作。现在,考虑下面的程序,计算同样的事情:

  1. >>> total = sum(len(t) for t in tokens)
  2. >>> print(total / len(tokens))
  3. 4.401...

第一行使用生成器表达式累加标示符的长度,第二行像前面一样计算平均值。每行代码执行一个完整的、有意义的工作,可以高级别的属性,如:“total是标识符长度的总和”,的方式来理解。实施细节留给 Python 解释器。第二段程序使用内置函数,在一个更抽象的层面构成程序;生成的代码是可读性更好。让我们看一个极端的例子:

  1. >>> word_list = []
  2. >>> i = 0
  3. >>> while i < len(tokens):
  4. ... j = 0
  5. ... while j < len(word_list) and word_list[j] <= tokens[i]:
  6. ... j += 1
  7. ... if j == 0 or tokens[i] != word_list[j-1]:
  8. ... word_list.insert(j, tokens[i])
  9. ... i += 1
  10. ...

等效的声明版本使用熟悉的内置函数,可以立即知道代码的目的:

  1. >>> word_list = sorted(set(tokens))

另一种情况,对于每行输出一个计数值,一个循环计数器似乎是必要的。然而,我们可以使用enumerate()处理序列s,为s中每个项目产生一个(i, s[i])形式的元组,以(0, s[0])开始。下面我们枚举频率分布的值,生成嵌套的(rank, (word, count))元组。按照产生排序项列表时的需要,输出rank+1使计数从1开始。

  1. >>> fd = nltk.FreqDist(nltk.corpus.brown.words())
  2. >>> cumulative = 0.0
  3. >>> most_common_words = [word for (word, count) in fd.most_common()]
  4. >>> for rank, word in enumerate(most_common_words):
  5. ... cumulative += fd.freq(word)
  6. ... print("%3d %6.2f%% %s" % (rank + 1, cumulative * 100, word))
  7. ... if cumulative > 0.25:
  8. ... break
  9. ...
  10. 1 5.40% the
  11. 2 10.42% ,
  12. 3 14.67% .
  13. 4 17.78% of
  14. 5 20.19% and
  15. 6 22.40% to
  16. 7 24.29% a
  17. 8 25.97% in

到目前为止,使用循环变量存储最大值或最小值,有时很诱人。让我们用这种方法找出文本中最长的词。

  1. >>> text = nltk.corpus.gutenberg.words('milton-paradise.txt')
  2. >>> longest = ''
  3. >>> for word in text:
  4. ... if len(word) > len(longest):
  5. ... longest = word
  6. >>> longest
  7. 'unextinguishable'

然而,一个更加清楚的解决方案是使用两个列表推导,它们的形式现在应该很熟悉:

  1. >>> maxlen = max(len(word) for word in text)
  2. >>> [word for word in text if len(word) == maxlen]
  3. ['unextinguishable', 'transubstantiate', 'inextinguishable', 'incomprehensible']

请注意,我们的第一个解决方案找到第一个长度最长的词,而第二种方案找到 所有 最长的词(通常是我们想要的)。虽然有两个解决方案之间的理论效率的差异,主要的开销是到内存中读取数据;一旦数据准备好,第二阶段处理数据可以瞬间高效完成。我们还需要平衡我们对程序的效率与程序员的效率的关注。一种快速但神秘的解决方案将是更难理解和维护的。

计数器的一些合理用途

在有些情况下,我们仍然要在列表推导中使用循环变量。例如:我们需要使用一个循环变量中提取列表中连续重叠的 n-grams:

  1. >>> sent = ['The', 'dog', 'gave', 'John', 'the', 'newspaper']
  2. >>> n = 3
  3. >>> [sent[i:i+n] for i in range(len(sent)-n+1)]
  4. [['The', 'dog', 'gave'],
  5. ['dog', 'gave', 'John'],
  6. ['gave', 'John', 'the'],
  7. ['John', 'the', 'newspaper']]

确保循环变量范围的正确相当棘手的。因为这是 NLP 中的常见操作,NLTK 提供了支持函数bigrams(text)trigrams(text)和一个更通用的ngrams(text, n)

下面是我们如何使用循环变量构建多维结构的一个例子。例如,建立一个 mn 列的数组,其中每个元素是一个集合,我们可以使用一个嵌套的列表推导:

  1. >>> m, n = 3, 7
  2. >>> array = [[set() for i in range(n)] for j in range(m)]
  3. >>> array[2][5].add('Alice')
  4. >>> pprint.pprint(array)
  5. [[set(), set(), set(), set(), set(), set(), set()],
  6. [set(), set(), set(), set(), set(), set(), set()],
  7. [set(), set(), set(), set(), set(), {'Alice'}, set()]]

请看循环变量ij在产生对象过程中没有用到,它们只是需要一个语法正确的for 语句。这种用法的另一个例子,请看表达式['very' for i in range(3)]产生一个包含三个'very'实例的列表,没有整数。

请注意,由于我们前面所讨论的有关对象复制的原因,使用乘法做这项工作是不正确的。

  1. >>> array = [[set()] * n] * m
  2. >>> array[2][5].add(7)
  3. >>> pprint.pprint(array)
  4. [[{7}, {7}, {7}, {7}, {7}, {7}, {7}],
  5. [{7}, {7}, {7}, {7}, {7}, {7}, {7}],
  6. [{7}, {7}, {7}, {7}, {7}, {7}, {7}]]

迭代是一个重要的编程概念。采取其他语言中的习惯用法是很诱人的。然而, Python 提供一些优雅和高度可读的替代品,正如我们已经看到。