The History of Python

2009年4月27日星期一

初创时期的语言设计与开发

英文原文链接:http://python-history.blogspot.com/2009/02/early-language-design-and-development.html

原文作者:Guido van Rossum

从ABC到Python

Python最初就受到ABC的影响且影响最深,ABC是Lambert Meertens、Leo Geurts和CWI其他人在1980年代初设计的编程语言。ABC的目的是成为一门教学语言、BASIC语言的替代者和个人计算机上的编程语言环境。ABC设计时先进行编程工作的任务分析,然后在包括严格用户测试的情况下再重复往返数遍。我个人在ABC开发组中的角色主要是语言及其集成编程环境实现。

Python中采用缩进的思想直接来自ABC,但是这个想法并不是ABC首创,缩进方式早已为Donald Knuth倡导并成为一种广泛的优良编程风格。(occam语言中也应用了缩进。)当然,ABC的作者确实发明了用冒号“:”来引导随后缩进块。在早期不使用冒号时的用户测试中,发现对于初次学习编程的新手来说,缩进的含义不够明确。添加冒号能明确缩进的含义:冒号提醒随后的程序块将要增加缩进,也将前后程序恰当的连接起来。

Python的主要数据类型也是在ABC的基础上修改而来。ABC的list实际上是bag或者说multiset,它利用修改过的B-tree实现,总是保持顺序。ABC的table则是总能保持key顺序的关联数组。我发现没有任何一种已有的数据类型适合表示从文件中逐行顺序读取内容,而我认为这是一种常见程序应用情况。(在ABC中勉强可以用行号为key的table来表示相应内容,但涉及行增删时处理困难。)于是我把list类型改为一种可有效处理增删的灵活数组,给用户完全控制list中元素顺序的能力。并提供了sort方法以备list需要排序时使用。

我还将保持key顺序的table改为hash表实现。我选择hash表的原因是因为我觉得它比ABC的B-tree速度更快也更容易实现。后者在理论上可以证明对于相当多的操作在时间和空间上是渐进优化的,然而实际情况是由于B-tree算法本身的复杂而难以正确实现。同样原因,对于规模较小的table,其性能也不出色。

我照搬了ABC的不可变tuple类型--Python中tuple的打包与解包操作直接从ABC中借来。由于tuple内部由数组实现,我决定给tuple加上数组常有的index(索引)和slice(切片)操作。

在给tuple增加数组操作接口时我发现需要解决tuple的边界情况,即tuple含有元素数目为0或者1的情况。我从ABC中引入的规则之一便是,对任一数据类型进行打印或转换为字符串时,其输出应当恰是语言语法分析器可有效处理的输入。所以,我需要有包含元素长度为0或者1的tuple的标记方法。同时我也不想失去单元素tuple和普通用括号包含表达式之间的区分能力,于是我采用了有些丑但却实用的方法,就是在括号中的表达式后附加一个多余的逗号,就变为单元素的tuple,用“()”来表示空tuple。需要说明的是,对于Python语法而言,tuple的括号并不是必须的,除了表示空元素这一情况时--我觉得用“空”表示空tuple太容易掩盖相应的书写错误了。

Python的string和ABC中的不可变string语义十分相近,但是有一点标记上的区别,Python采用从0起始的索引。现在已经有了三种可以索引的类型,list、tuple和string,我决定将其归纳为sequence作为他们的通用概念。以使对任意sequence类型的核心操作形式相同,包括获取长度(len(s)),索引(s[i]),切片(s[i:j]),和遍历(for i in s)等。

数值类型是我背离ABC设计思路最远的部分之一。ABC在运行时有两种类型的数值类型:可表示任意精度有理数的精确数和增大表示范围的二进制浮点数所表示的近似数。我没有采用前者那种有理数表示方式。(轶事:有次我想用ABC来计算自己要交的税。看起来很正常的程序,计算几个简单数字却费了很长时间还没算出来。又仔细看了下才发现正在处理几千位数字精度的算数运行,全然不顾最终输出结果只要精巧到荷兰盾的元和分即可。) 对于Python,我因此选择了更传统的方式,采用机器整数和机器二进制浮点数来表示。Python的实现中,这两种数据类型简单对应了C数据类型中的long和double。

考虑到对无上界精确数的需求也很有实际意义,我增加了一个大数类型,称为long。我当时已经有了一个大数类型,是我几年前为了改进ABC实现时的半成品(ABC最初的相应实现,也是我对ABC做的最早贡献之一,是用十进制作为内部表示的)。因此,将代码拿来给Python用对我来说是再合适不过的事情了。

虽然我为Python添加了大数类型,这里一定要明确说明的是我并不希望所有的整数运算都使用大数类型。根据我和CWI的同事对Python程序的profiling结果,我知道整数运算占据了大多数程序整个运行时间的相当比例。显然,整数的最常用情况是对内存可容纳的sequence类型进行索引操作。因此,我预测机器整数对各种常见情况是实用的,只有在做“严肃的数学”或者用分来计算美国国债时才使用大数类型。

数值类型的问题

对数值类型的实现,特别是整数类型,是我犯了严重设计错误之处,也让我在如何设计Python方面获得了经验教训。

既然Python有两种整数类型,我需要在程序中区分两种类型的方法。我的解决方式是当用户需要long类型时,自行对数字添加L后缀(例如1234L)。用户对不必要的实现细节无需操心是ABC的设计哲学,这里是Python违反这一思想的地方之一。

悲哀的是,这还只是一个更大严重问题的冰山一角。一个相当夸张的错误是在特定情况下,integer和long两种整数实现会有语义上的细微不同!因为int类型是由机器整数表示,向上溢出时结果只是简单的截断为低32位或其它某个C中long类型对应的精度。此外,int类型通常情况下是有符号数,在位操作、位移操作、和8进制/16进制互相转换时则当做无符号数。而相对应的,long类型则总是有符号数。因此,某些操作会因为参数是由int还是long类型表达而产生不同的结果。例如,在32位运算中,1<<31(1左移31位)是一个32位的大负数,而1<<32结果为0。然而1L<<31(long类型的1左移31位)产生一个long类型整数,值为2**31,1L<<32的结果为2**32。

为了解决这个问题,我做了一些简单的修补。对以前简单截断结果的操作做出变更,我将大多数算术运算结果超过存储范围时改为抛出溢出异常(raise an OverflowError exception)。(唯一的例外是上文提到的“位运算”操作,我认为用户期望这儿的结果和C中对应运算保持一致。)如果没有这个检查,Python用户注定在书写代码时依赖一个以2**32为模的有符号二进制运算(正如C中那样),修正这个错误对社区的转换来说着实是一件苦事。

虽然包含溢出检查看起来只是实现上一个细微末节,然而调试时的痛苦经历让我认识到这是一个有用的特征。作为我早期Python编程经验之一,我尝试实现了计算“Meertens数”的简单数学算法,这个消遣数学算法是由Richard Bird为纪念ABC首席发明人在CWI满25年时发明的。最前面几个Meertens比较小,但是算法转化为代码时,我当时还不知道中间计算结果远大于32位。这占用我大量时间和精力才找到问题根源,于是我立刻决定通过检查所有整数的运算来解决问题,只要结果不能用C语言中long表示就抛出异常。溢出检查的额外耗费由于我在实现运算结果需要生成新的对象产生较大耗费而显得影响有限。

悲哀的是,我还要继续道歉,抛出异常仍然不是正确的解决方案!当时我受C语言的“T数值类型上运算返回结果也是T数值类型”这一规则约束。这条规则也是导致我在整数语义上另一个重大错误的原因:截断整数除法的结果,这个我在后面的博文中会谈到。放个马后炮,我应该让溢出的int整数操作结果升级为long类型。这也是今天Python采用的方式,可惜这个转换用了太长时间。

尽管在数值设计方面遇到这些问题,这个经历还是产生了一个十分有益的结果。我决定Python中应当不含有未定义的结果--相反,当计算结果不正确时,总是抛出异常。这样Python程序员就不会因为运算中内部的悄悄转换而遭遇失败。这是一个重要语言设计原则,无论对语言本身还是标准库来说都是如此。

没有评论: