4.3 风格的问题

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

Python 代码风格

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

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

>>>if(len(syllables)>4andlen(syllables[2])==3and
...syllables[2][2]in[aeiou]andsyllables[2][3]==syllables[1][3]):
...process(syllables)
>>>iflen(syllables)>4andlen(syllables[2])==3and\
...syllables[2][2]in[aeiou]andsyllables[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
>>>fortokenintokens:
...count+=1
...total+=len(token)
>>>total/count
4.401545438271973

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

>>>total=sum(len(t)fortintokens)
>>>print(total/len(tokens))
4.401...

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

>>>word_list=[]
>>>i=0
>>>whilei
...j=0
...whilej
...j+=1
...ifj==0ortokens[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=[wordfor(word,count)infd.most_common()]
>>>forrank,wordinenumerate(most_common_words):
...cumulative+=fd.freq(word)
...print("%3d %6.2f%% %s"%(rank+1,cumulative*100,word))
...ifcumulative>0.25:
...break
...
15.40%the
210.42%,
314.67%.
417.78%of
520.19%and
622.40%to
724.29%a
825.97%in

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

>>>text=nltk.corpus.gutenberg.words('milton-paradise.txt')
>>>longest=''
>>>forwordintext:
...iflen(word)>len(longest):
...longest=word
>>>longest
'unextinguishable'

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

>>>maxlen=max(len(word)forwordintext)
>>>[wordforwordintextiflen(word)==maxlen]
['unextinguishable','transubstantiate','inextinguishable','incomprehensible']

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

计数器的一些合理用途

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

>>>sent=['The','dog','gave','John','the','newspaper']
>>>n=3
>>>[sent[i:i+n]foriinrange(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()foriinrange(n)]forjinrange(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 提供一些优雅和高度可读的替代品,正如我们已经看到。