翻译Python历史:早期语言的设计与开发

This post a a Chinese translation of Guido van Rossums’s article “Early Language Design and Development” on his blog named “The History of Python”.

原文地址:http://python-history.blogspot.com/2009/02/early-language-design-and-development.html

从ABC到Python

Python最早的、最重要的影响来源是ABC,一门由Lambert Meertens,Leo Geurts和其他CWI的人在1980年代早期设计的语言。ABC是用于教学的语言,是个人计算领域里BASIC语言的替代物。它被设计成先做编程任务的任务分析,再做许多包含严格用户测试的迭代。我在ABC小组中的任务主要是实现语言和它的集成编辑环境。

Python的缩进样式直接来自ABC,但这个想法不是最早不是从ABC开始的–它已经被Donald Knuth所提倡成为了著名的程序风格概念。(occam语言也用这种格式。)然而,ABC的作者发明了由冒号来分隔引导句子和缩进内容。在早期的用户测试没有冒号方式后,我们发现没有冒号的版本的缩进含义让初学者在开始的时候搞不清楚。冒号的添加显著的使语言清晰了:冒号引起了人们对下文的注意,并且使上下的段落处于正常的位置。

Python的主要的数据类型也继承自ABC,尽管其中的一些有了改变。ABC的列表(list)是真正的包或者多重集合,使用了一种修改了的B-树来保证它总是有序的。它的表格(table)是联合的数组,简单的按照key排序。我觉得所有的数据类型都不适合来表达,例如,从一个文件里读入几行被认为是一个常用案例。(在ABC里你要用一个table来储存这些行,而这些行是通过行号来作为key的,但这样依赖插入和删除就很复杂。)所以我把列表类型变成了一个有插入删除操作的灵活的数组,使用户可以完全的控制元素在列表里的顺序。sort方法给出偶尔需要的有序结果。

我同时也把有序的表格的实现换成了哈希表。我选择哈希表因为我相信这样比ABC的B-树实现更快、更简单。后来从理论上证明了对于各种操作在空间和时间消耗上B-树渐渐的更优化,但从实践上来说,由于B-树的复杂性,这难以被正确的实现。同样的道理,小型表格的效率也成了次级优化。

我保留了ABC的不可变的元组(tuple)类型–Python的元组打包和解包操作是直接取自ABC的。因为在内部元组是用数组表示的,我决定添加像数组一样的索引和切片。

给元组添加像数组一样的接口的结果之一是我必须想出一些方法来解决当元组长度为0或1时的情况。我从ABC那里获得的一条原则是每个数据类型在打印或转换成字符串的时候,应该被表示成合法的语言分析程序的输入的形式。所以,我需要对长度为0和长度为1的元组做记号。同时,我不想让单一元组和没有圆括号的表达式之间没有差别,因此我了一个丑陋但实用的方法:逗号把一个表达式变成长度为一的元组,“()”则代表零长度的元组。在Python的元组语法中,圆括号并不经常用到,除了这里:我觉得用“没有”来表示空元组太容易让真正的排版模糊了。

Python的字符串与ABC的字符串用非常相似的(不可变的)语义开头,但用了不同的符号和基于0的索引。因为我目前有三种可索引的类型:列表、元组、和字符串,我决定把它们泛化成一个通用概念:序列。这个泛化使一些核心操作变得非常可靠,如获得长度(len(s))、索引(s[i])、分片(s[i:j])、和迭代(for i in s)都运作在任何序列类型上。

数字是与ABC不同的地方之一。在运行期,ABC有两种数字类型:exact型表示任意精度的有理数,approximate型表示有扩展指数范围的二进制浮点数。我没有选择有理数类型。(轶事:一次我尝试着用ABC来计算我的税务。程序看上去非常直白,但用了太长的时间去计算少量简单的数字。经过调查,我发现它用上千位的精确度来运算数字,但他们最终在输出的时候被四舍五入到盾(荷兰货币单位)和分。)因此我为Python选择了更传统的机器整数和机器二进制浮点数模型。在Python的实现中,这些数字分别被简单的表示成C语言中的long类型和double类型。

感觉到无限精确的数字有它的重要性,我加入了大数(bignum)类型,并把它称作long。我在几年前改进ABC的实现的时候已经实现了大数(在ABC的原始实现中,我对ABC的最初的贡献之一是使用了内部表示的十进制数)。因此,我在Python中用了这段代码。

尽管我在Python中加入了大数,我还是要强调我不想把大数运用在所有的整数运算上。通过我和在CWI的同事们写的优化Python的程序中,我知道整数操作表示了在多数程序中占了绝大多数。到目前为止,整数最常见的用途是在内存索引序列。因此,我猜想机器整数被用在绝大多数的情况下;而大数仅仅被用在“严密数学”或者计算以分来计的国家债务上面。

数字的问题

数字的实现,尤其是整数,是我犯了许多严重设计错误的一个方面。但我也在思考Python设计的时候学到了很多。

由于Python有两种不同的整数类型,我需要想出一种方法来在程序中分辨它们。我的方案是让用户在需要long型数的时候明确的在数字的末尾加上L(如1234L)。这是Python违反ABC的用户不需要关心底层细节实现的一个方面。

不幸的是,这只是许多大程序的一小部分细节。一个更过分的错误是我对于整型(integer)和长整型(long)的实现在某些例子上有略微不同的语义!因为int类型是用机器整数表示的,运算溢出时会默默的翻转32位数,就像C的long类型那样。另外,int类型一般是有符号数,但在按位运算和移位运算时是按照无符号数来算的,并会被转化成八进制和十六进制表示。另一方面,长整型数永远被认为是有符号数。因此,在参数分别是int和long类型时,一些运算会产生不同的结果。例如,一个32位运算1<<31(1往左移31位)会产生最大的32位负整数,1<<32会产生0,但1L<<31(长整型1往左移31位)的结果是长整型数2的31次方,1L<<32的结果是2的32次方。

为了解决这个问题,我作了简单的修正。我让多数运算操作在结果无法存下时抛出OverflowError异常。(唯一的例外是上面提到的“位运算”操作,我假设用户希望的是C语言里这些操作的行为。)在没有添加这个检查时,Python用户无疑已经开始写依赖符号二进制模运算2**32的语义(像C语言那样),但修改这个问题增加了社区用户转换时的困难。

尽管添加溢出检查看上去只是一个微小的细节变化,但痛苦的调试经验让我认识到这是个有用的功能。作为我早期的Python编程实验之一,我试过实现一个简单的数学算法来计算“Meertens数”,一个由Richard Bird为了CWI里ABC作者的第25年庆发明的消遣数学算法。前几个Meertens数很小,但当我把算法翻译成代码时,我意识到计算的中间结果比32位大的多。在我发现这一点前,它耗费了我又长又痛苦的时间来调试。于是我那时决定解决所有整数操作溢出的问题,并当结果不能用C语言的long类型表示的时候抛出异常。溢出测试的额外消耗比我实现为新结果选择分配对象时花费的总开销要小。

不幸的是,我难过的说抛出溢出异常也不是正确的解决方案。那时候,我正在沉迷在C语言的规则“对数字类型T的操作返回T类型的结果”。这条规则也是我在整数语义中的犯的另一点大的错误:切断整数除法的结果,我会在后面的blog文章中讨论这一点。事后来看,我应该让会溢出的整数操作把它们的结果提升成为long类型。这是今天Python工作的方式,但用了很长时间来做出这个转换。

除了数字问题,我从这次经验中获得了一个非常正面的事情。我决定Python中不应该有没有定义结果的值,反之,在无法运算正确的返回值时,总应该抛出异常。因此,Python程序不会因为未定义的值无声的在底层传递而出错。这对语言本身和标准库来说,仍然是相当重要的一点。

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据