编程风格:程序设计与系统构建的艺术(原书第2版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.3 评注

此编程风格的灵感来自Forth语言(一种小型编程语言),该语言最初由Charles Moore在20世纪50年代后期作为个人编程系统开发(当时,Moore是Smithsonian天体物理实验室的一名程序员)。该编程系统是一种简单语言的解释器,无须重新编译程序便可处理不同的公式,而编译程序在当时是一项非常耗时的任务。

这种有趣的小型语言的核心是引入了栈的概念。公式按后缀表示法录入系统,例如“3 4+”(表示3加4)。系统以每次一个操作数的频率将操作数压入栈,根据运算符从栈上提取操作数,运算,并且用结果替代操作数,然后压入栈。当数据没有立刻被需要时,则将其放在被称为堆的内存部分。除了堆栈机器之外,Forth语言还支持定义过程(Procedure,在Forth语言中叫“单词”)。这些过程与内置过程一样,对栈上的数据进行操作。

由于后缀表示法和一些在任何其他语言中都不使用的特殊符号,Forth语言的语法可能很难理解。但是,一旦我们理解了约束——栈、堆、过程和名称,语言模型就变得非常简单。本章没有使用由Python语言编写的Forth语言解释器,而是展示了如何在Python程序中编码Forth语言的底层约束,从而大致形成Forth语言的编程风格。我们来分析一下示例程序。

首先,为了支持这种风格,我们定义栈(第7行)和堆(第12行)[1]。接着,我们定义一组过程(在Forth语言术语中叫“单词”)。这些过程可以帮助我们将问题分解为更小的子步骤,例如读取文件(第17行到第25行)、过滤字符(第27行到第36行)、扫描单词(第38行到第45行)、删除停用词(第47行到第66行)、计算频率(第68行到第90行),以及对结果进行排序(第92行到第94行)。接下来,我们将详细地研究其中的一些过程。需要注意的一件事是,所有这些过程都使用栈中的数据(例如第22、36、45行),并通过将数据压入栈(例如第24、36、45行)结束。

在Forth语言中,堆(heap)能够支持数据块的分配,并且这些数据块可以(而且通常是)被绑定到名称。换句话说,就是被绑定到变量。该机制相对较底层,因为程序员需要定义数据的大小。在我们对Forth风格的模拟中,我们简单地使用字典(第12行)。因此,在第56行中,我们将栈上的停用词直接弹出,并存放到堆上定义的名为stop_words的变量中。

示例程序的许多部分未使用Forth风格编写,但有些部分符合Forth语言的风格,我们重点关注后者。例如,过程remove_stop_words(从第47行开始)删除停用词。当该过程被调用时,栈中包含输入文件的全部单词(并且这些单词被进行了适当的规范化)。《傲慢与偏见》的前几个词是:

对于那本书,以上就是此时栈中的内容。接下来,我们打开停用词文件,并将停用词列表压入栈(第51行到第55行)。为了简单起见,我们将它们保存在自己的列表中,而不是将它们与栈中的其余数据合并。栈现在看起来像这样:

我们把文件中的全部停用词读取完毕并压入栈后,再将它们弹出并存放到堆中(第56行),为下一步处理仍存放在栈中的单词做准备。第60行到第64行以如下方式遍历栈中的(每个)单词,直到栈为空(在第60行进行测试):我们检查栈顶部的单词(在Python中为stack[-1])是否在停用词列表中(第61行)。在真正的Forth语言中,这个测试比这里显示的要底层得多,因为我们也需要显式地遍历停用词列表。在任何情况下,如果这个词在停用词列表中,只需将它从栈中弹出并忽略它。如果该词不在停用词列表中(第63行),则将其从栈中弹出并存放到堆中另一个名为words的变量中——该列表不断累积非停用词(第64行)。当迭代结束时,我们获取变量words并将其全部内容放回栈中(第65行)。最终得到包含所有非停用词的栈,如下所示:

此时,我们不再需要堆上的变量,所以释放掉它们(第66行)。Forth语言支持从堆中删除变量,正如我们现在所做的。

frequencies过程(从第68行开始)展示了与算术运算相关的另一个风格细节。该过程从处理栈上的非停用词开始(如上所示),并以将包含单词和频率的字典压入栈中结束(第89行)。它的工作原理如下。第一步,在堆上分配一个名为word_freqs的变量,该变量用来存储单词-词频对(第73行),该变量初始为空。第二步,开始遍历栈上的全部单词,对于栈顶部的每个单词,检查该单词之前是否出现过(第78行)(出于性能原因,本示例代码使用了比Forth语言更高级的表达方式),如果该单词之前出现过,则需要递增它的频率计数,它的实现方式是将该单词当前的频率计数压入栈(第80行),接着将值1压入栈(第81行),然后将栈顶部的两个操作数弹出并相加后再将结果压入栈;如果该单词之前未出现过(第83行),则只需将值1压入栈。最后,我们将频率计数(第86行赋值操作的右侧)和单词本身(第86行赋值操作的左侧)从栈中弹出,存入堆上的变量中,并且将下一个单词移到栈顶部,直到栈为空(回到第75行)。最终,如前所述,我们将堆上变量中的全部内容压入栈,并删除该变量。

始于第98行的main函数是整个程序的开始。首先,我们将输入文件的名称压入栈(第98行),并按顺序调用各个过程。请注意,被调用的这些过程之间并不是完全独立的。每一个过程都假设栈上现存的数据是前一个过程处理完的。

一旦计数和排序过程完成,我们就打印出结果(第105行到第109行)。这段代码展示了Forth语言中“无限循环”(即一直运行,直到条件为真才停止)的风格细节。在本示例中,我们想要遍历存放单词和词频的字典变量,直到25次迭代为止。因此,我们执行以下操作。首先,将数字0压入栈中(第102行),在已经存在的数据(词频)之上,然后进行无限循环,直到栈顶部达到数字25。在每次迭代中,我们将栈中的计数弹出到一个变量中(第106行),将下一个单词和词频从栈中弹出并打印出来(第107行)。然后,将变量中的计数压回栈,再压入值1,将它们相加,从而有效地递增计数。当栈顶部的值为25时,循环和程序终止。第二个终止子句(len(stack)>1)用于可能甚至没有25个单词的小型测试文件。

还有许多选项可以采用,我们鼓励读者探索这种风格的各种解决方案。