为什么选择 Python?
我对 Python 的初次接触是一个偶然,而且当时我并不太喜欢我所看到的。那是 1997 年初,Mark Lutz 的著作《Programming Python》(由 O'Reilly & Associates 出版)刚刚面世。O'Reilly 的书籍偶尔会送到我家门口,它们是从组织内部某位神秘恩人从新书中挑选出来的,挑选过程是随机的,我已放弃理解。
其中一本是《Programming Python》。我发现这有点意思,因为我收集计算机语言。我精通二十多种通用语言,编写编译器和解释器只是为了好玩,并且自己设计了许多专用语言和标记形式。我最近完成的项目,在我写这篇文章时,是一种名为 SNG 的专用语言,用于处理 PNG(便携式网络图形)图像。感兴趣的读者可以访问 SNG 的主页:http://www.catb.org/~esr/sng/。我还将几种奇怪的通用语言的实现写在了我的 Retrocomputing Museum 页面上,http://www.catb.org/retro/。
我已经听说了一些关于 Python 的信息,我知道现在它被称为“脚本语言”,一种解释型语言,具有自己的内置内存管理和良好的工具,可以调用其他程序并与之协作。因此,我深入研究了《Programming Python》,脑海中最重要的一个问题是:它有什么是 Perl 没有的?
当然,Perl 是现代脚本语言中的巨头。它在很大程度上取代了 shell,成为系统管理员首选的脚本语言,这部分归功于其全面的 UNIX 库和系统调用集,部分归功于活跃的 Perl 社区构建的大量 Perl 模块。据估计,这种语言是互联网上约 85% 的“实时”内容背后的 CGI 语言。它的创造者 Larry Wall 被公认为开源社区最重要的领导者之一,并且在当前的黑客神殿中,通常排名仅次于 Linus Torvalds 和 Richard Stallman。
那时,我曾将 Perl 用于许多小型项目。我发现它非常强大,即使语法和语言的其他一些方面看起来相当随意,如果不小心使用,很容易出现问题。在我看来,Python 作为另一种脚本语言,还有很长的路要走,所以当我阅读时,我首先寻找它与 Perl 有什么不同之处。
我立刻被 Python 的第一个奇怪特性绊倒了,每个人都会注意到:空格(缩进)在语言语法中实际上很重要。该语言没有 C 和 Perl 花括号语法的类似物;相反,缩进的变化分隔了语句组。而且,像大多数黑客第一次意识到这个事实时一样,我本能地反感。
我勉强足够老,在 1970 年代用批处理 FORTRAN 编程了几个月。现在的黑客大多不是这样,但不知何故,我们的文化似乎保留了关于那些旧式固定字段语言有多糟糕的相当准确的民间记忆。事实上,当时用来描述 Pascal 和 C 中较新的标记导向语法的术语“自由格式”几乎已经被遗忘;几十年来,所有语言都是这样设计的。或者几乎所有,无论如何。看到 Python 的这个特性,最初的反应就像不小心踩到一堆冒着热气的恐龙粪便一样,这很难责怪任何人。
我当然就是这种感觉。我粗略地浏览了其余的语言描述,没有太大的兴趣。我没有看到太多其他可以推荐 Python 的地方,除了也许语法似乎比 Perl 更简洁,并且用于执行按钮和菜单等基本 GUI 元素的工具看起来相当不错。
我把书放回书架,并在心里记下,我应该用 Python 编写一些小型以 GUI 为中心的项目,以确保我真正理解了这门语言。但我不相信我所看到的会有效地与 Perl 竞争。
许多其他事情共同作用,使这个笔记在我的优先级列表中靠后了许多个月。1997 年的剩余时间对我来说是多事之秋;其中一件事是,那一年我撰写并出版了最初版本的《大教堂与集市》。但我确实抽出时间编写了几个 Perl 程序,包括两个规模和复杂程度都相当大的程序。其中一个,keeper,仍然用作 Metalab 软件存档中归档传入提交的助手。它生成了您在 metalab.unc.edu/pub/Linux/!INDEX.html 看到的网页。另一个,anthologize,用于自动生成 Linux 文档项目 HOWTO 存档中 Linux 第六版的 PostScript。这两个程序都可以在 Metalab 上找到。
编写这些程序让我对 Perl 越来越不满意。更大的项目规模似乎将 Perl 的一些烦恼放大为严重、持续的问题。在一百行代码中显得仅仅是古怪的语法,在一千行代码中开始看起来像一道难以穿透的荆棘篱笆。“不止一种方法可以做到”在小规模上增加了趣味性和表现力,但使得在更广泛的代码库中维护一致的风格变得更加困难。而且,后来为了解决更大程序(对象、词法作用域、“use strict”等)的复杂性控制需求而修补到 Perl 中的许多特性都给人一种脆弱、临时拼凑的感觉。
这些问题结合起来,使得大量的 Perl 代码在仅仅离开几天后,就变得异常难以阅读和整体把握。此外,我发现我花费越来越多的时间与语言的特性作斗争,而不是我的应用程序问题。而且,最糟糕的是,结果代码很丑陋——这很重要。丑陋的程序就像丑陋的悬索桥:它们比漂亮的程序更容易倒塌,因为人类(尤其是工程师)感知美的方式与我们处理和理解复杂性的能力密切相关。一种使编写优雅代码变得困难的语言,也使编写优秀代码变得困难。
凭借二十多种语言的经验,我可以检测到语言设计已被推向其功能极限的所有迹象。到 1997 年年中,我开始思考“肯定有更好的方法”,并开始寻找一种更优雅的脚本语言。
我没有考虑的一个方向是回到 C 作为默认语言。在新程序中进行自己的内存管理的时代早已过去,除非是在少数专业领域,如内核黑客、科学计算和 3D 图形——在这些领域,您绝对必须获得最大的速度和对内存使用的严格控制,因为您需要尽可能地压榨硬件。
对于大多数其他情况,接受缓冲区溢出、指针别名问题、malloc/free 内存泄漏以及所有其他相关弊端的调试开销,在今天的机器上简直是疯了。最好是用少量的周期和少量的内存来换取脚本语言内存管理器的开销,并节省更宝贵的人类时间。事实上,这种策略的优势正是自 1990 年代中期以来推动 Perl 爆炸性增长的原因。
我曾短暂地尝试过 Tcl,但很快发现它比 Perl 更难扩展。作为一名老 LISPer,我也研究了 Lisp 和 Scheme 的各种当前方言——但是,正如 Lisp 的历史惯例一样,许多巧妙的设计几乎因稀缺或不存在的文档、对 POSIX/UNIX 工具的不完整访问以及规模虽小但却深度分裂的用户社区而变得毫无用处。Perl 的流行并非偶然;它的大多数竞争对手要么在大型项目中比 Perl 更糟糕,要么在某种程度上远不如其理论上优越的设计应该使其发挥的作用。
我对 Python 的第二次审视几乎和第一次一样是偶然的。1997 年 10 月,fetchmail-friends 邮件列表上的一系列问题清楚地表明,最终用户在为我的 fetchmail 实用程序生成配置文件时遇到了越来越多的麻烦。该文件使用简单的、经典的 UNIX 自由格式语法,但当用户在多个站点拥有 POP3 和 IMAP 帐户时,可能会变得非常复杂。例如,请参见清单 1,了解我的配置文件的简化版本。
我决定通过编写一个用户友好的配置编辑器 fetchmailconf 来解决这个问题。fetchmailconf 的设计目标很明确:完全将控制文件语法隐藏在时尚、符合人体工程学的 GUI 界面之后,该界面配有选择按钮、滑块和填写表格。
想到用 Perl 实现这个目标,我并不感到兴奋。我见过 Perl 中的 GUI 代码,它是 Perl 和 Tcl 的尖锐混合体,看起来比我自己的纯 Perl 代码还要丑陋。正是在这个时候,我想起了六个多月前我记下的笔记。这可能是一个获得 Python 实践经验的机会。
当然,这让我再次面对 Python 的 pons asinorum,空格的重要性。然而,这一次,我勇往直前,粗略地为一些示例 GUI 元素编写了一些代码。奇怪的是,大约二十分钟后,Python 对空格的使用不再让人感到不自然。我只是缩进代码,就像我在 C 程序中一样,它就工作了。
这是我的第一个惊喜。我的第二个惊喜是在项目开始几个小时后,当我注意到(考虑到查找《Programming Python》中新特性所需的暂停)我生成 工作 代码的速度几乎与我的打字速度一样快。当我意识到这一点时,我非常吃惊。编码中衡量工作量的一个重要指标是,您编写的东西实际上与您对问题的心理表示不符的频率,并且在意识到您刚刚键入的内容实际上不会告诉语言执行您正在思考的操作时,必须回溯。衡量良好语言设计的一个重要指标是,随着您对语言经验的积累,此类失误的百分比下降的速度有多快。
当您编写工作代码的速度几乎与您的打字速度一样快,并且您的失误率接近于零时,这通常意味着您已经掌握了这门语言。但这没有道理,因为这仍然是第一天,我还在经常停下来查找新的语言和库特性!
这是我的第一个线索,表明在 Python 中,我实际上是在处理一个非常好的设计。大多数语言在其设计中都内置了如此多的摩擦和笨拙之处,以至于您在失误率降至接近于零之前很久就学会了它们的大部分特性集。Python 是我使用过的第一种通用语言,它颠倒了这个过程。
并非我花了很长时间才学会特性集。我用了六个工作日编写了一个可用的 fetchmailconf,带有 GUI,其中大约相当于两天的时间用于学习 Python 本身。这反映了该语言的另一个有用的特性:它是紧凑的——您可以将它的整个特性集(以及至少一个库的概念索引)放在您的脑海中。C 是一种著名的紧凑型语言。Perl 以其臭名昭著的非紧凑而闻名;“不止一种方法可以做到!”这个概念使 Perl 付出的代价之一是失去了紧凑的可能性。
但我最戏剧性的发现时刻还在后头。我的设计有一个问题:我可以很容易地从用户的 GUI 操作中生成配置文件,但编辑它们是一个更难的问题。或者,更确切地说,将它们读入可编辑的形式是一个问题。
fetchmail 配置文件语法的解析器相当复杂。它实际上是用 YACC 和 Lex 编写的,这两个经典的 UNIX 工具用于在 C 中生成语言解析代码。为了使 fetchmailconf 能够编辑现有的配置文件,我认为它必须在 Python 中复制该复杂的解析器。我非常不愿意这样做,部分原因是工作量很大,部分原因是我不确定如何确定两种不同语言的两个解析器是否接受相同的语法。我最不需要的就是在配置语言发展时,还要额外付出劳动来保持两个解析器的同步!
这个问题困扰了我一段时间。然后我突然想到一个主意:我让 fetchmailconf 使用 fetchmail 自己的解析器!我在 fetchmail 中添加了一个 --configdump 选项,该选项将解析 .fetchmailrc 并将结果以 Python 初始化器的格式转储到标准输出。对于上面的文件,结果大致如下清单 2(为了节省空间,省略了一些与示例无关的数据)。
然后,Python 可以评估 fetchmail --configdump 输出,并将配置作为变量“fetchmail”的值提供。
这还不是舞步的最后一步。我真正想要的不仅仅是让 fetchmailconf 拥有现有的配置,而是将其转换为活动对象的链接树。此树中将有三种类型的对象:Configuration(表示整个配置的顶层对象)、Site(表示要轮询的站点之一)和 User(表示附加到站点的用户数据)。示例文件描述了五个站点对象,每个站点对象都附加了一个用户对象。
我已经设计并编写了这三个对象类(这就是花费四天时间的原因,其中大部分时间都花在了使小部件的布局恰到好处)。每个对象类都有一个方法,使其弹出一个 GUI 编辑面板来修改其实例数据。我剩下的最后一个问题是如何将此 Python 初始化器中的静态数据转换为活动对象。
我考虑编写代码,该代码将显式了解所有三个类的结构,并使用该知识来遍历初始化器,创建匹配的对象,但我拒绝了这个想法,因为随着配置语言添加新功能,新的类成员可能会随着时间的推移而添加。如果我以显而易见的方式编写对象创建代码,那么当类定义或初始化器结构发生更改时,它将变得脆弱且容易不同步。
我真正想要的是能够分析初始化器的形状和成员、查询类定义本身关于其成员,然后调整自身以阻抗匹配这两组的代码。
这种东西被称为元类黑客,通常被认为是可怕的深奥——深奥的黑魔法。大多数面向对象的语言根本不支持它;在那些支持它的语言中(Perl 就是其中之一),它往往是一项复杂而脆弱的任务。到目前为止,Python 的低摩擦系数给我留下了深刻的印象,但这是一个真正的考验。我需要与语言进行多长时间的搏斗才能让它做到这一点?从以前的经验来看,我知道这场战斗可能会很痛苦,即使假设我赢了,但我还是投入到书中,阅读了关于 Python 元类工具的内容。生成的函数如清单 3 所示,调用它的代码如清单 4 所示。
对于深奥的黑魔法来说,这看起来还不错,不是吗?三十二行,包括注释。仅仅从我知道的关于类结构的信息来看,调用代码甚至是可以阅读的。但这段代码的规模并不是真正的令人震惊之处。做好心理准备:这段代码只花了我大约九十分钟的时间编写——而且它在第一次运行时就正确地工作了。
要说我感到惊讶,那简直是轻描淡写。当简单技术的实现第一次运行时就完全按照预期工作时,就已经很了不起了;但是我在一种新语言中进行的第一次元类黑客攻击,从冷启动开始六天后?即使我们假设我是一位相当有才华的黑客,这也是对 Python 设计的清晰度和优雅性的惊人证明。
我根本不可能在 Perl 中完成这样的政变,即使我在该语言中拥有极其丰富的经验。正是在这个时候,我意识到我可能要抛弃 Perl 了。
这是我最戏剧性的 Python 时刻。但是,总而言之,这只是一个聪明的黑客技巧。一种语言的长期实用性不在于它支持巧妙的黑客技巧的能力,而在于它如何出色且不引人注目地支持日常编程工作。日常编程工作主要不是编写新程序,而是主要阅读和修改现有程序。
因此,故事的真正的要点是:在编写 fetchmailconf 数周和数月后,我仍然可以阅读 fetchmailconf 代码并理解它的作用,而无需付出严重的脑力劳动。而我不再为任何东西编写 Perl,除了小型项目之外,真正的原因是在我编写大量 Perl 代码时,情况从来都不是这样。我害怕将来不得不修改 keeper 或 anthologize——但 fetchmailconf 完全不会让我感到不安。
Perl 仍然有它的用途。对于涉及大量文本模式匹配的小型项目(100 行或更少),我仍然更倾向于修补基于 Perl 正则表达式的解决方案,而不是求助于 Python。有关此类事物的近期优秀示例,请参见 fetchmail 发行版中的 timeseries 和 growthplot 脚本。实际上,这些非常类似于 Perl 在其作为 awk/sed/grep/sh 的某种组合的原始角色中所做的事情,在它拥有函数和直接访问操作系统 API 之前。对于任何更大或更复杂的事情,我已经开始喜欢 Python 的微妙优点——而且我认为您也会如此。
本文中提及的所有清单都可以通过匿名下载文件 ftp.linuxjournal.com/pub/lj/listings/issue73/3882.tgz 获得。