4.3 风格的问题
编程是作为一门科学的艺术。无可争议的程序设计的“圣经”,Donald Knuth 的 2500 页的多卷作品,叫做《计算机程序设计艺术》。已经有许多书籍是关于文学化编程的,它们认为人类,不只是电脑,必须阅读和理解程序。在这里,我们挑选了一些编程风格的问题,它们对你的代码的可读性,包括代码布局、程序与声明的风格、使用循环变量都有重要的影响。
Python 代码风格
编写程序时,你会做许多微妙的选择:名称、间距、注释等等。当你在看别人编写的代码时,风格上的不必要的差异使其难以理解。因此,Python 语言的设计者发表了 Python 代码风格指南,httphttp://www.python.org/dev/peps/pep-0008/
。风格指南中提出的基本价值是一致性,目的是最大限度地提高代码的可读性。我们在这里简要回顾一下它的一些主要建议,并请读者阅读完整的指南,里面有对实例的详细的讨论。
代码布局中每个缩进级别应使用 4 个空格。你应该确保当你在一个文件中写 Python 代码时,避免使用 tab 缩进,因为它可能由于不同的文本编辑器的不同解释而产生混乱。每行应少于 80 个字符长;如果必要的话,你可以在圆括号、方括号或花括号内换行,因为 Python 能够探测到该行与下一行是连续的。如果你需要在圆括号、方括号或大括号中换行,通常可以添加额外的括号,也可以在行尾需要换行的地方添加一个反斜杠:
>>> if (len(syllables) > 4 and len(syllables[2]) == 3 and
... syllables[2][2] in [aeiou] and syllables[2][3] == syllables[1][3]):
... process(syllables)
>>> if len(syllables) > 4 and len(syllables[2]) == 3 and \
... syllables[2][2] in [aeiou] and syllables[2][3] == syllables[1][3]:
... process(syllables)
注意
键入空格来代替制表符很快就会成为一件苦差事。许多程序编辑器内置对 Python 的支持,能自动缩进代码,突出任何语法错误(包括缩进错误)。关于 Python 编辑器列表,请见http://wiki.python.org/moin/PythonEditors
。
过程风格与声明风格
我们刚才已经看到可以不同的方式执行相同的任务,其中蕴含着对执行效率的影响。另一个影响程序开发的因素是 编程风格 。思考下面的计算布朗语料库中词的平均长度的程序:
>>> tokens = nltk.corpus.brown.words(categories='news')
>>> count = 0
>>> total = 0
>>> for token in tokens:
... count += 1
... total += len(token)
>>> total / count
4.401545438271973
在这段程序中,我们使用变量count
跟踪遇到的词符的数量,total
储存所有词的长度的总和。这是一个低级别的风格,与机器代码,即计算机的 CPU 所执行的基本操作,相差不远。两个变量就像 CPU 的两个寄存器,积累许多中间环节产生的值,和直到最才有意义的值。我们说,这段程序是以 过程 风格编写,一步一步口授机器操作。现在,考虑下面的程序,计算同样的事情:
>>> total = sum(len(t) for t in tokens)
>>> print(total / len(tokens))
4.401...
第一行使用生成器表达式累加标示符的长度,第二行像前面一样计算平均值。每行代码执行一个完整的、有意义的工作,可以高级别的属性,如:“total
是标识符长度的总和”,的方式来理解。实施细节留给 Python 解释器。第二段程序使用内置函数,在一个更抽象的层面构成程序;生成的代码是可读性更好。让我们看一个极端的例子:
>>> word_list = []
>>> i = 0
>>> while i < len(tokens):
... j = 0
... while j < len(word_list) and word_list[j] <= tokens[i]:
... j += 1
... if j == 0 or tokens[i] != word_list[j-1]:
... word_list.insert(j, tokens[i])
... i += 1
...
等效的声明版本使用熟悉的内置函数,可以立即知道代码的目的:
>>> word_list = sorted(set(tokens))
另一种情况,对于每行输出一个计数值,一个循环计数器似乎是必要的。然而,我们可以使用enumerate()
处理序列s
,为s
中每个项目产生一个(i, s[i])
形式的元组,以(0, s[0])
开始。下面我们枚举频率分布的值,生成嵌套的(rank, (word, count))
元组。按照产生排序项列表时的需要,输出rank+1
使计数从1
开始。
>>> fd = nltk.FreqDist(nltk.corpus.brown.words())
>>> cumulative = 0.0
>>> most_common_words = [word for (word, count) in fd.most_common()]
>>> for rank, word in enumerate(most_common_words):
... cumulative += fd.freq(word)
... print("%3d %6.2f%% %s" % (rank + 1, cumulative * 100, word))
... if cumulative > 0.25:
... break
...
1 5.40% the
2 10.42% ,
3 14.67% .
4 17.78% of
5 20.19% and
6 22.40% to
7 24.29% a
8 25.97% in
到目前为止,使用循环变量存储最大值或最小值,有时很诱人。让我们用这种方法找出文本中最长的词。
>>> text = nltk.corpus.gutenberg.words('milton-paradise.txt')
>>> longest = ''
>>> for word in text:
... if len(word) > len(longest):
... longest = word
>>> longest
'unextinguishable'
然而,一个更加清楚的解决方案是使用两个列表推导,它们的形式现在应该很熟悉:
>>> maxlen = max(len(word) for word in text)
>>> [word for word in text if len(word) == maxlen]
['unextinguishable', 'transubstantiate', 'inextinguishable', 'incomprehensible']
请注意,我们的第一个解决方案找到第一个长度最长的词,而第二种方案找到 所有 最长的词(通常是我们想要的)。虽然有两个解决方案之间的理论效率的差异,主要的开销是到内存中读取数据;一旦数据准备好,第二阶段处理数据可以瞬间高效完成。我们还需要平衡我们对程序的效率与程序员的效率的关注。一种快速但神秘的解决方案将是更难理解和维护的。
计数器的一些合理用途
在有些情况下,我们仍然要在列表推导中使用循环变量。例如:我们需要使用一个循环变量中提取列表中连续重叠的 n-grams:
>>> sent = ['The', 'dog', 'gave', 'John', 'the', 'newspaper']
>>> n = 3
>>> [sent[i:i+n] for i in range(len(sent)-n+1)]
[['The', 'dog', 'gave'],
['dog', 'gave', 'John'],
['gave', 'John', 'the'],
['John', 'the', 'newspaper']]
确保循环变量范围的正确相当棘手的。因为这是 NLP 中的常见操作,NLTK 提供了支持函数bigrams(text)
、trigrams(text)
和一个更通用的ngrams(text, n)
。
下面是我们如何使用循环变量构建多维结构的一个例子。例如,建立一个 m 行 n 列的数组,其中每个元素是一个集合,我们可以使用一个嵌套的列表推导:
>>> m, n = 3, 7
>>> array = [[set() for i in range(n)] for j in range(m)]
>>> array[2][5].add('Alice')
>>> pprint.pprint(array)
[[set(), set(), set(), set(), set(), set(), set()],
[set(), set(), set(), set(), set(), set(), set()],
[set(), set(), set(), set(), set(), {'Alice'}, set()]]
请看循环变量i
和j
在产生对象过程中没有用到,它们只是需要一个语法正确的for
语句。这种用法的另一个例子,请看表达式['very' for i in range(3)]
产生一个包含三个'very'
实例的列表,没有整数。
请注意,由于我们前面所讨论的有关对象复制的原因,使用乘法做这项工作是不正确的。
>>> array = [[set()] * n] * m
>>> array[2][5].add(7)
>>> pprint.pprint(array)
[[{7}, {7}, {7}, {7}, {7}, {7}, {7}],
[{7}, {7}, {7}, {7}, {7}, {7}, {7}],
[{7}, {7}, {7}, {7}, {7}, {7}, {7}]]
迭代是一个重要的编程概念。采取其他语言中的习惯用法是很诱人的。然而, Python 提供一些优雅和高度可读的替代品,正如我们已经看到。