30 秒看懂
Python 3.11 发布前夕,CPython 团队承诺新版本比上一版快 10% 到 60%。这份提速来自让解释器做专门化优化,没有加入 JIT。在与莱克斯的长谈里,吉多把这次提速放进了他三十年来的一条论证里:编程语言是一份菜谱,对机器必须无歧义,对之后要调试它的人又必须易读。他仍然为缩进辩护,把 GIL 称为恰到好处的折中(Goldilocks),并描绘了一个 Python 4.0:它改动的是 C ABI(二进制接口),语法则保持不变。
- 1吉多给出的定义很直接:编程语言就是一份菜谱,是一份足够精确的指令,只允许一种读法。
- 2口语里的歧义是特性,并非缺陷这是人与人交流那场舞蹈的一部分:律师靠它收费,恋人靠上下文领会彼此,没有人会要求编译器去定义“爱”。
- 3软件开发从 1940 年代末起就是一项社交活动独自待在实验室里的疯狂科学家并不存在,就算存在,等他打完字,前面几行是什么意思他也忘了。
- 4吉多补充了第四种失败:程序 99% 的时间表现良好,却在某个边缘情况下突然发疯。他说,这几乎就是现在所有软件的状态;差的软件,那 99% 还会掉下去很多。
- 5Python 的缩进是语法本身的一部分,它不仅仅是风格提示。其他语言能编译一团挤在一行的乱代码,Python 不行;吉多说,如果能重新开始,他仍然会选缩进。
- 6往前看几十年,他预计 Python 会变成一个大多数人叫不上名字的遗留层,就像线粒体渗透在生物学里,却不进入日常生活的视野。
按章节阅读完整文稿
CPython 是仍然站着的最后一个 Python,而编程语言是写给两类读者的菜谱
Python 3.11 即将发布时,CPython 团队提出了一个在任何工程领域都很醒目的说法:新版本会比上一版快 10% 到 60%。莱克斯问吉多,这是怎么做到的,而 CPython 究竟是什么。吉多没有先谈提速,而是先修正了这个名词。“CPython 是最后一个仍然存在的 Python 实现,也是历史上最早出现的那一个,”他说,指的是他三十多年前开始写的那个最初的解释器。名字里的 C,代表它是用 C 语言写成的。编程语言的默认实现,是用另一种语言写的程序。外行听来觉得古怪,程序员却觉得理所当然。莱克斯提醒说,这一期节目需要同时面向两类听众。
莱克斯把听众设想成一个思想实验:一条船漂在大洋中央,船上两个人。一个人从没听说过编程,只喜欢钓鱼;另一个是硅谷工程师,写过 C、C++、Python、Rust 和 Java,能把编程语言的发展史倒背如流。吉多先选了钓鱼的那位。“在海上,他显然是更有用的那个人,”他说,“你得让他高兴。”这样的听众有一部手机,对手机里发生的事大概心存疑虑,至少听说过里面有一台小小的计算机。编程语言,就是告诉这台计算机该做什么的代码。至于二进制、汇编和寄存器,吉多不打算在这里展开。讨论人类语言时,没有人会从声道构造和舌头的位置讲起。
吉多给编程语言下的定义相当直白:它是一份类似菜谱的指令清单。拿来一条面包,划开,取出两片:一片涂上芥末,另一片涂上果酱,接着夹肉,最后再放奶酪。莱克斯说,科学课老师常玩一个残忍版本的游戏:学生写下做三明治的步骤,老师严格照字面执行,直到全班被迫写出毫无歧义的英语。这个游戏点出了自然语言与代码之间的距离。口语里的歧义是一种特性,算不上缺陷,它正是人与人交流时那场舞蹈的一部分。律师把歧义当作可以计费的东西,其他人则靠上下文绕过去。对一个重要的人说“我爱你”,对方不会把这句话拿去编译,然后回一句“请定义 love”。“我想,我妻子和我儿子对这句话的理解大概很不一样,”吉多说,可这三个字明明是一样的。
莱克斯转而谈到 PEP 8,也就是风格指南,以及几代 Python 程序员都背过的那句话:可读性很重要。代码是给机器执行的,机器不在乎空格,也不在乎注释里有没有脏话。那么,可读性究竟是为谁准备的?吉多换了个角度来回答。从 1940 年代末开始,软件就是一种社会性的活动。独自待在实验室里写出天才代码的疯狂科学家,只是个神话。即便真有这样的人,他打字也快不到哪里去,等程序写完,前面几行是什么意思,他自己也早忘了。一本菜谱的作者只写一遍,印十万册,面向的是厨房里的业余厨师。程序却同时有两类读者:一类是需要精确性的计算机,另一类是需要调试和修改它的程序员,包括一年后回头看代码的你自己。樱桃派的菜谱没有这么复杂,软件则复杂得多,所以你永远不可能一次就把它做对。
调试其实是三个不同的问题,却被塞进了同一个词里
莱克斯请吉多给调试下个定义,吉多的回答分成几层。最浅的一层是蠢错误:少写一个字母,括号没配对,路径写错了。中间一层是逻辑错误:程序完全照你写的去执行,但你写的本身就是错的想法,比如算法选错了,或者某个不变量没有成立。最深的一层,如莱克斯所说,带点哲学意味:代码没有问题,逻辑也站得住,运行又快又稳,可它就是没用。你做出了一件严格按指令运行的东西,而这件东西本来就不该存在,或者它的做法让产品无法使用。
吉多还补充了第四层,他希望单独提出来。程序可能有 99% 的时间表现很好,然后在某个边缘情况下突然怪异起来。“现在几乎所有软件都是这样,”他说,“指的是好软件。”如果是差的软件,那 99% 会掉下去很多。这里有一个不太舒服的结论:你接触过的每一个系统,包括支撑银行、医院的系统,都有一些从没有人走过的路径。这个行当的成熟,在很大程度上就在于学会在这些路径上设置护栏,而不是假装它们不存在。
迫使两类读者的设计成为必要的,不只是复杂性。你总是在不断改进樱桃派的菜谱,可能是和一群人一起改,也可能是去年那位疯狂科学家回来添了一层糖霜。在大多数行业,风格规则听起来像玩笑,在软件里却属于语言的核心文档,因为这份菜谱会被改写、交接,也会在压力下被反复阅读。按吉多的说法,调试其实是对整个循环的称呼:错别字,错误的想法,做出错误的东西,再加上只在生产环境里才会冒出来的边缘情况。
在 Python 里缩进就是语法,吉多仍然会选它
人们初次接触 Python,首先注意到的往往是它的样子:代码块一级一级向内收进,像一份排版讲究的商务文件。吉多的解释很朴实。文字要能读下去,靠的是空间。把菜谱上的条目符号全删掉,再把句子挤成一大段,到了厨房里,你就分不清哪些是食材、哪些是步骤,也看不出一步在哪里结束。缩进让分组看得见:一段文字里包着一段更小的,再包着一段更小的。缩进得太深,走到第二三层就没地方写字了;缩进得太浅,又分不清顶层条目和第三层条目。
四个空格是一个妥协,它来自老式排版的惯例,也契合 1980 年代显示器的典型宽度,两方面约束的交集正好落在四这个数字上。这并不是定理。谷歌的 Python 风格指南用的是两个空格,吉多认为谷歌的 C++ 代码也是两个空格。不习惯这种写法的人读起来会比较费劲,因为在这门语言里,高层结构完全由缩进决定,缩进一浅,骨架就模糊了。经典的 Unix 则走向另一个极端,用一个 tab 八格,吉多觉得这看着很别扭,三层缩进就把整行占满了。在 C、Rust、C++ 和 Java 里,缩进只是建议:编译器照样接受一整行写完的乱代码,只是同事和 IDE 会吼你。Python 不一样。你可以缩进两个空格,也可以缩进十二个,但同一个代码块里的内容必须对齐。缩进属于语法的一部分,风格建议的说法在这里太轻了。
莱克斯问了一个历史问题:是否存在一个平行宇宙,里面的 Python 用花括号来划分代码块?“当然有,”吉多说。他一度坚信缩进就是更好的。即使抛开上下文来看,他仍然这样认为,因为缩进能减少视觉杂乱。可惜上下文几乎决定一切,而大多数程序员都熟悉好几门语言。除了 Python,或许还有 Fortran,Java、Rust、C、C++、JavaScript、TypeScript 和 Perl 都用花括号标记代码块,只有 Python 是例外。从 Java 转过来的程序员,第一天写 Python 时会觉得别扭,就像走路时突然发现自己一直在下意识地甩手。那为什么不改?“不可能了,”吉多说,“Python 里的花括号已经用来做别的事了,主要是字典。”即便大家都同意新语言应该用缩进,你也没法把 C 改成用缩进。语言里的这类结构,就像承重墙,动不了。
莱克斯把思想实验推到了最后:如果吉多能回到三十年前,不考虑兼容性,从头设计 Python,他还会选缩进吗?会,理由有三。第一,缩进省下了一对成对的括号,而世上的成对符号总是不够用,省下的括号可以拿去做别的事,在 Python 里后来也确实这样做了。第二,对没学过编程的人来说,缩进稍微容易些。一门十三周的入门课,内容既有算法和数据结构,也有语法、变量这些基础概念,还要涉及循环、函数、递归、类,以及表达式与运算符,分号则是教学中最经典的时间陷阱。Python 没有那种分号,你既不会忘记它,也不会把它放错地方。第三,吉多主动补充说,这种强制的严格也在教新手:编程是讲究细节的事,不能像写英语那样随手下笔。当然,新手要操心的细节还有很多,但这条信息总会传达到。
美元符号是一块内存不够用的机器留下的化石
莱克斯换了个靶子:PHP 在变量前面加的美元符号。在他看来,这东西既不好解析,也烦人,还很容易打错。吉多用考古的方式回答。这个符号来自一条谱系,包括 PHP、Perl 和 Unix shell,再往前还有 IBM 的作业控制语言。最早的 shell 能运行脚本,脚本就是几行文本,每一行是一条命令。一个很小的处理器读取时,把行首的词当作程序名,剩下的整行当作文本交给程序自己去解析。没有参数传递的概念。等人们想要变量,尤其是想要参数时,就撞上了一个冲突:写下 program x y,原本的含义就是 x 是一个文件名。可你想表达的是:x 是个变量,它的值才是文件名。这两种意思必须能区分开。
到了 1950 年代和 1960 年代,脚本解释器本身必须是一个很小的程序,小到能装进一小块内存。省事的做法是:逐个字符扫描,一旦遇到美元符号,就跳转到另一段代码,把它后面直到空格为止的字符吞下,当作变量名。同一个符号也便于人阅读:二十行之后,看到 $xyz,人就知道它是参数,而不是磁盘上的路径。这些遗留物如今仍藏在 Facebook 规模的后端系统之后,吉多还提到,维基百科的大量内容背后也有它。莱克斯把它比作 DNA:DNA 的内部机制已经稳定了几亿年,里面充满古怪之处,要是从零设计一个能自我复制的分子,你绝不会那样设计。
莱克斯认为,生物学里有许多奇怪的冗余,但在有机体层面依然很有韧性,代码则不然。吉多不同意。他说,现代代码的韧性之强,会让你吃惊。即便是经过充分测试、实际运行良好的软件,里面也有很多地方是坏的,只不过这些错误被一层层的纠错机制吸收掉了,其中包括最后一层:用户。让用户重启电脑,问题就消失了,这也是系统的一部分。温和一点的版本是刷新页面,大家遇到怪事时,通常都会刷新两三次,才承认事情确实不对劲。莱克斯说,人们应该改掉多点一下的习惯,因为第二下往往只是把灯又关了。吉多的结论更干脆:那就刷三次。莱克斯仍然认为美元符号是“语法毒药”,是一台伪装成文档的 bug 制造机。吉多则两边都认:它的确算是一种文档,同时也是 bug 的来源。
成熟软件大约每千行一个 bug,初稿则是每千行七十个
莱克斯想象了一下:如果把核武器控制代码,和 Twitter、Facebook、Dropbox、谷歌、Windows 的代码并排放进一张表,每千行的 bug 数会是什么样子。吉多说,这要看你读没读过相关研究。他记得的几项研究,多数来自 1980 年代和 1990 年代,涵盖不同语言、不同公司、不同风格,结论相当稳定:在被公认为已经做到极致的成熟软件里,大约每千行一个 bug。莱克斯接着引用了一家编程分析公司的报告,视角在开发者这一端:平均每位开发者每千行代码会写出 70 个 bug,其中 15 个会流到客户手里;修复一个 bug 所花的时间,是写一行代码的 30 倍;开发者有 75% 的时间花在调试上;美国每年在查找和修复 bug 上要花掉 1130 亿美元。吉多的第一反应是,他自己的估计偏了一个数量级。接着他提出了真正要紧的区别:每千行一个,描述的是经过测试、已经定型的软件;每千行七十个,描述的则是你刚开始敲键盘的那一刻。两个数字衡量的根本不是同一样东西。
他还把这一页当成营销材料来读。页面底部有一个“联系我们”的按钮,意思是只需花一点不到 1130 亿美元,供应商就能替你把问题解决。他说,能让修 bug 的投入一笔勾销的银弹,是不会出现的。莱克斯想打开 Stack Overflow 上关于同一主题的报告,页面却显示网站维护中。“这挺讽刺的,”吉多说。接着,他解释了 70 和 1 之间的差距从哪里来。软件开发是高度迭代的。没有人会真的做一份“一年后发布什么”的完整计划,然后把每个细节都定死,因为这些细节本身就是程序,而程序的计划同样会有错误和不准确之处。人们实际做的是:敲一段代码,运行,看看结果是不是自己想要的,不对就改,改完再跑。几十轮下来,剩下的残留,才是“每千行一个”想要描述的东西。代码在诞生时是脏的,工程的全部工作,就是把它洗干净。
吉多主动承认,自己是个糟糕的打字员。他十根手指都用,但用得很差,从来没有上过打字课。他第一次认真学习 QWERTY 键盘布局,是在大学里,那时用的是打孔卡,他用两根手指一下一下地敲出代码。莱克斯有个现场观察的办法:让任何一位程序员演示四行代码,然后数他按了几次退格键。有些人,尤其是有观众在场的时候,为了修正行首的一个错字,会删掉二三十甚至四十个字符。很多人打错一个二十个字符的单词,就整个删掉重打,有时要重打三四次。吉多说,打错一个字并且马上改掉,严格来说算不上 bug,只是耽误了时间,偶尔还会有错字漏进后面的十行代码里。忘记初始化变量则属于另一类错误。在 Python 里,这类问题通常一运行就会暴露,现代编译器也能提前抓出很多。真正麻烦的是它的表亲:变量你确实初始化过,名字看上去也对,但初始化的是另一个东西,或者名字和别的变量撞了。这种错误编译器抓不到,测试也未必覆盖得到,它会一直潜伏着,等到某个深夜在生产环境里冒出来。
莱克斯用的是 Kinesis 键盘,因为退格键位于拇指正下方。普通键盘的退格键在小指下面,手要伸很远才够得到,长年累月下来,会形成一种人们已经察觉不到的疼痛。吉多问,要是在别人那里只有普通键盘,怎么办。莱克斯的回答很实际:他可以一辈子都随身带着这把键盘。坐飞机时,只要预计要写代码,他就会打开笔记本,再从包里拿出一把 Kinesis 放在旁边。“你得接受自己是什么样的人,”吉多说。舒适的配置确实存在,有人看电影非得盖一条特定的毯子,莱克斯则需要这把键盘,才能进入专注的状态。他也尽量使用最先进的 IDE,但真正能让他安心坐下来的地方,仍然是 Emacs。在顺手的工具上抓得多紧,又该在新工具上投入多少,这种张力是一辈子都会和自己反复争论的问题。
老工具死了,概念却留下了;你花掉的那些年并非总是白费
莱克斯说,社区传来的信号很嘈杂。每年都有一群人为新工具兴奋,你得判断它们是在定义新一代的工作方式,还是像天气一样刮一阵就过去。前端领域最为明显:JavaScript 框架、打包工具和状态管理方案,一波接一波地来,又一波接一波地走。他自己也有过这样的经历。他在 Flash 里学过 ActionScript,包括图形动画,还写过不少 Java applet,当时老实说真以为它们会是 Web 的未来。结果并不是。吉多既没有宽慰他说那些时间并非白费,也没有把那些工作说成垃圾。具体的技术会被替换,但它最先引入、或让大众第一次接触到的许多概念会留下来。Java applet 早已不在了,可“页面里嵌着一小段代码,响应按钮、链接或鼠标悬停”这个想法,并没有消失。Flash 做动画非常繁琐,后来 JavaScript 的同类方案是更好的做法,但动画本身还在。“没有螺旋桨飞机,就不会有喷气式飞机,”他说。
从用户的角度看,这种演化很容易表示认同。从程序员的角度看,莱克斯想要的是更难算的那笔账:泡在 ActionScript 里的夜晚,熬在 Java applet 调试上的深夜。Java 的知识后来迁移过去了,Flash 的没有。吉多说,如果你足够早学 ActionScript,那在当时是非常宝贵的技能,只是现在它已经不再有需求。这正是你选择新语言和新 IDE 时的计算方式。年轻时,你还有大把人生,应该鼓励自己做一点蠢事:别被弄死或弄残,但要敢于偏离。十次里有九次,你会明白为什么别人不这么干;有一次,你会找到更好的东西。一旦结了婚、有了孩子,你会变得更厌恶风险,因为要守护的东西更多了,而且希望你已经在更早的时候,有过疯狂实验的窗口期。早期的全押选择,比如 PHP、.NET、JavaScript,看上去像是一辈子站队,但这只在技术保持相关性的前提下成立。如果你十六岁学的是 C,二十六岁时 C 已经死了,你仍然有时间转向。
莱克斯讲了自己一次艰难的转向。他大半辈子都用 C 和 C++ 写程序,手里握着那把锤子,看什么都像钉子,包括脚本。进入谷歌后,在 TensorFlow 成为显而易见的技术栈之前,他就看出 C++ 并不适合机器学习,这既关乎语言,也关乎社区和文化。转向 Python 让他感觉像一场生存危机,像离开一个深爱的人。C++ 至今仍是地球上使用最广泛的语言之一,还在持续演化,如果你让它把你拖进去,它也是一个元编程的兔子洞。在你已经深爱着某门语言的时候,很难去问的问题是:你用哪门语言最有效率,又想和哪门语言一起过日子。他仍然用 PHP 写 WordPress 后端,也仍然在脑子里转着同一个三角:前端是 JavaScript,所以用 Node;他的非网络工作大多是 Python,所以用 Python;大部分网络仍然跑在 PHP 上,而且经验已经付过学费,所以用 PHP。没有唯一正确的答案。它取决于时间、人生阶段、手头的工作,以及你喜欢哪些社区。
回头看二十年,你可以说 ActionScript 那段弯路白走了。当时没人能预料到。吉多的建议是接受并不是每个选择都完美,留一个 B 计划,不要像挑股票那样用表格估算每门语言能给你带来多少终身收入。莱克斯反驳:如果你仔细分析自己能在哪里产生影响,有些语言选择确实更好,而那关乎社区和发展轨迹,不关乎语法。吉多反对的是这里的输入。“更好”这个词最难界定;对你更好,不等于对别人更好。社区的质量在事后很难衡量,事前更难预测。你可以凭直觉去判断,直觉也可能出错。这也是为什么世界上有几十亿人。湾区里想建立谷歌规模公司的人,比世界上能容纳谷歌规模公司的位置要多,他们之间必然会打一场,而编程语言只是其中的一个选择。
Python 3.11 靠猜测明天的类型与今天相同而变快
吉多开玩笑说,那个钓鱼的人早就跳下船了。程序员重新拿回了 CPython。项目计划继续推进,从 3.8、3.9、3.10、3.11,一直到 3.99,之后大概会是 3.100。Python 3.11 最吸引人的宣称是速度。除了请来优秀的团队之外,怎么让一个三十年的老解释器跑得更快?吉多回到了他最初的做法。他当年没料到会成名,他只想在三个月左右做出一个能用的、有用的东西,所以走了捷径,在语言设计和实现两方面都借鉴了别人的想法,并尽量把代码写得简单。很多事情可以通过增加代码来提高效率,这是一种时间与空间的权衡。简单的版本每次都从头计算,看上去显然正确,其他程序员也容易读懂。随着需求和性能压力增加,你会用更多内存重写,记住之前的结果。他在课堂上举的例子是素数:朴素的循环会检查 n 能否被小于它的每一个整数整除;稍微聪明一点的循环在平方根处停止,偶数只检查一次,最后只用已有的素数去检查。要超过那种写法,你得先有好几个离散数学博士学位。字符串匹配也是同样的故事。人们最先发明的往往是更简单的算法。设计一门语言时,你会面对几百个小问题,每一个都需要一个解法,才谈得上拥有一门语言。
CPython 是一个解释器,可以说是一份关于菜谱的菜谱:给定一段程序的文本,我们该怎样运行它。大家都说 Python 是解释型语言,但里面其实有一个编译器。它不编译成机器码,而是编译成字节码,目标是一台想象中的机器,也就是 Python 解释器本身。在 3.11 版本中,他们只调整了这个编译器的一些细节。他们没有做即时编译器(JIT),那是 Java 世界的招数,如今几乎所有解释型语言都在用。他们做的是让解释器本身更高效。核心思想是一个自适应的专门化解释器。字节码看起来像一台想象中的 CPU 的机器指令,可以调用函数、把两个数相加、打印一个值。但在 Python 里,编译时并不存在“把两个数相加”这种确定的操作。编译器不知道你加的是整数,你加的可能是字符串、列表,或者两个实现了 + 的自定义类的实例。那个加号是一种界面技巧:用一个符号表示一类函数。
旧的 ADD 字节码是一层层的指针。查找对象的类型,询问该类型是否定义了加法,找到函数指针,调用它;函数再检查另一个参数的类型,做一大堆错误处理,然后才能把比特位加在一起。整数、浮点数和字符串对同样的比特有不同的解释。这种优化只针对一行代码的局部。如果你调用某个函数一千次,那个加号很可能每次都是整数相加,或者每次都是字符串拼接。于是每次加法运行时,解释器会记下一笔小账:实际完成工作的是哪种类型。经过几次相同的命中之后,它会把通用的加法改写成一个“整数加法”操作,假定两边都是整数,并直接在原地完成计算。吉多说,这里的“谎言”在于:Python 之后仍然可能在同一行遇到浮点数、字符串或混合类型的组合。专门化的操作依然会检查两个参数是不是整数,并用一些技巧让检查变得很便宜;如果猜错了,就退回到通用的加法。猜错时你会浪费几个周期。统计上,你希望大部分时候猜对,因为如果猜错太频繁,程序反而会稍微变慢。有人可以构造一个反例,让它在 3.11 上比 3.10 慢五倍。那会是一个极端的意外,也是一个好玩的逆向工程任务。
这个启发式的想法简单得几乎让人尴尬:假设明天的天气和今天一样。这已经比随机猜测强很多了。Python 不是第一个这样做的语言,这个技巧在其他解释型、动态类型语言中已经很有名,要提速它们时都用过,包括 Facebook 为 PHP 做的 HHVM 相关论文,以及 JVM 和学术界的一些想法。它之所以重要,正是因为 Python 是动态的。如果编译器能读出 x 和 y 都是整数,它就可以直接生成一条硬件加法指令。而在 Python 里,你通常不声明类型,甚至不声明变量是否存在,它们在第一次赋值时就会凭空出现,这对初学者很友好,对解释器却代价高昂。吉多说,3.11 面临的挑战始终是同一个:在不失去这门语言超级动态特性的前提下,让解释执行更高效。
类型提示是解释器不肯信任的一种代码检查工具
Python 3.5 引入了 PEP 484 的类型提示。莱克斯问它们是什么,解释器会不会用到它们。吉多说,类型提示是可选的,在拥有超大型 Python 代码库的大公司里特别流行。它们不只是文档:它是一套子语言,你可以声明这个变量是整数,这个参数是字符串,这个函数返回一个字符串列表。运行代码时,它们不会被检查。另有一个静态类型检查器,不执行代码,只阅读源码,认真推断代码看上去在做什么,再拿注解来核对。它是一种开发工具,类似于代码检查器(linter)。注解不用来加速解释器,原因有几个。很多人不用它们。用的人有时会在注解里“撒谎”:检查器无法证明某个整数永远不会不是整数,而运行时仍然有人违反这个假设,解释器却照常运行。如果 Python 开始强制执行注解,很多程序会停止工作,还有一些程序根本不可能存在,因为它们太动态了。将来有一种可能:再过三四五个版本,注解会变成整数加法指令的提示,并仍然保留通用的回退路径。但现在还没有到那一步。
第三方库已经可以接受这些提示,并在运行时强制执行,几乎像手动的断言。Python 的内省能力非常强:程序运行时,你可以询问变量的类型,询问函数的参数,现在还可以询问它们的类型注解。代价则与 3.11 那个项目相反:额外的运行时检查会让程序变慢。考虑到上面那个“开发者 75% 的时间花在调试上”的说法,吉多有时宁愿牺牲性能,也要抓住类型错误;而在大多数情况下,在上线前用静态方式检查,比在运行时零敲碎打地检查更有效率。Mypy 是最初的静态检查器,由芬兰开发者 Jukka 发起,吉多认为他发明它的时候人还住在英格兰。PEP 484 的语法就是随这个检查器一起发展出来的。Jukka 起初用的是另一种语法,类似 C++ 和 Java 泛型里的尖括号,但那在 Python 里会太别扭。当时他们已经有一个用于注解的占位语法,还没有约定好的含义,而类型显然是最自然的用途。语言里当时仍然没有直接写泛型的语法,所以早期的 mypy 字面上就是“我的 Python”:一个先做类型检查、再把额外标记剥掉的预处理器。预处理器在典型的 Python 工作流里很别扭。吉多指出,如果那种模式胜出,它本可以变成类似 TypeScript 的那种分离。
被问到是否会推荐 TypeScript 或 JavaScript 时,吉多选了 TypeScript,因为它的严格性让你清楚代码在做什么,有助于编辑,而且保持了兼容。纯 JavaScript 库可以在 TypeScript 里使用,反过来也一样,这种兼容性是 TypeScript 成功的关键。在 JavaScript 世界里,转译器已经是标准工作流,所以 TypeScript 需要转译这件事不算大问题。一个处于第一阶段的提案,会让 JavaScript 在运行时忽略 TypeScript 注解语法的一个超集,就像 Python 忽略提示一样,这意味着将来你也许可以把 TypeScript 直接放进 JS 引擎,不必经过转译这一步。如今浏览器更新引擎的速度很快,直接转译 JavaScript 本身的重要性也就降低了。莱克斯觉得,与 Python 相比,JavaScript 在每行 bug 数上“吃掉了蛋糕”:这是一片在高速演化中的美丽乱局。吉多说,文化差异一部分是随机的,一部分则来自环境:JavaScript 几乎是客户端 Web 应用唯一的语言,要部署到各种形状的屏幕上,这就形成了一个与 Python 不同的社区,而 Python 有着更长的大型稳定系统历史。
吉多说,Python 中的静态类型仍然有争议,但比 mypy 和 PEP 484 刚出现的时候已经被接受得多了。Mypy 是用 Python 写的,后来的大部分类型特性都是先在它里面做原型。它的成功足以让公司认定这个方向值得投入,于是,谷歌、Facebook,以及后来的微软都没有去资助 mypy 本身,而是各自做了自己的检查器。Facebook 的 Pyre 用 OCaml 写成,出自做过 HHVM 的团队之手,很贴合 Facebook 的工作流。谷歌的 pytype 用 Python 写,是为一个巨大的单一代码仓库打造的。微软押注的是 Pyright。从理念上说,类型检查器应该位于语言之外,像一个专注的代码检查器:编译器不会告诉你某个变量没有被用到,但 linter 会,同时还有 PEP 8 命名检查,以及一堆启发式规则。目前没有人热衷于把检查器整合进 Python 本身。检查器仍然按月发布,而 Python 一年才发布一次,语法一旦加进去,要删掉往往要经过很多个版本。吉多不愿给出采用率的数字,但任何有持续集成流程的人,大概都会在每次提交时跑一个检查器,而在大多数情况下,那个检查器仍然是 mypy。莱克斯找到一个页面,声称 20% 到 30% 的 Python 3 代码库在使用类型提示,似乎是抽样 GitHub 得出的。
VS Code 是 Emacs 精神上的继承者,PyCharm 则是一辆十八轮大卡车
莱克斯问了一个带点偏向的问题,因为吉多现在在微软工作:写 Python 最好的 IDE 是哪个?PyCharm、VS Code、Vim,还是 Emacs?吉多早先用 vi,之后从 1980 年代初一直用 Emacs,直到录制前大约两年。大约在 2013 年到 2018 年之间,他也尝试过 PyCharm,主要是为了其中几个 Emacs 里没有的功能。他说,PyCharm 就像开一辆十八轮的大卡车。Emacs 则像一辆开了十几万英里、每一个咯吱声都熟悉的舒适丰田。他在 Emacs 里很自在,但至少按他的配置,它找函数定义的能力并不很好。
在 Dropbox 时,他要探索一个五百万行的 Python 代码库。用 grep 搜一个叫 Fubar 的类,会找到很多同名的类。PyCharm 把仓库索引完毕之后,这类导航非常有用。但只要一开始要编辑,他就会跳回 Emacs,那里他打字更快,而且当他已经知道要哪个文件时,切换文件也快得多。他从没真正习惯 PyCharm 的界面。莱克斯承认自己也有同样的分裂,也有同样的内疚:因为没有学快捷键而觉得自己像个脾气古怪的老头,就像如果你要做大量键盘输入,却拒绝学习十指打字,长远看会限制你。他甚至从纸书改用 Kindle,好让自己的大脑习惯数字化的未来。莱克斯说,PyCharm 是功能最复杂、最完备的 Python IDE;有时他觉得自己应该被禁止使用别的东西,比如 VS Code,或者为了这份工作放弃 Emacs。
吉多不会替别人做这个选择。他刚到微软时试着用了一阵 VS Code,确实很喜欢,然后花了一段时间才弄明白原因。VS Code 的创造者未必同意,但在他看来,VS Code 是 Emacs 精神上的继承者。Emacs 的核心在于,它大部分是用 Lisp 写的:新功能就是新的 Lisp 包,你为这些功能升级,而不是为核心 C 代码里某个晦涩的改动去升级。一个很小的引擎负责读取文件、把字符画到屏幕上、管理缓冲区;这个核心中的几乎所有东西仍然可以被覆盖。VS Code 有类似的架构:一个几乎只有其所有者才会修改的基础引擎,一个包管理器,一套包与引擎之间、包与包之间如何通信的接口和约定,以及光标、选区和输入的基本操作。它的可扩展性和包生态映照着 Emacs。JetBrains 的 IDE,比如 PyCharm 和 PHPStorm,都是 IntelliJ 的特化版本,同一个编辑引擎套了不同的外壳。第三方扩展并非不可能,但要费很大力气,也不是这种文化的一部分。几年前,他想为 PyCharm 做更好的 mypy 集成,写插件的工程师连文档和调试流程都很难找到,那次体验并不愉快。
并行靠的是额外的硬件;并发是一种程序员必须让它保持诚实的错觉
莱克斯读过吉多的一篇文章《推理 asyncio.Semaphore》,开头写的是硅谷的一家快餐店只有一张桌子。这个例子是真的吗?吉多说,这只是个比喻,是他难得的一次文学尝试,用一个古怪的例子先抓住注意力,再讲关于锁和信号量的枯燥内容。(莱克斯查过:这类餐厅确实存在,多在东京,只有一张桌子,要排队,饭菜是专门为你做的。)对那位回到船上的钓鱼者来说,并发就像两根鱼竿。坐等式的钓鱼,大部分时间是在等鱼咬钩;一个人可以同时守着三四根竿子,同一个周六能钓到更多的鱼。在计算机里,这两个词是分开的。说到并行计算机,通常指的是几块完整的 CPU 被连在一起,共享内存或 I/O 总线。并发则可能更抽象:一种让事情看似同时发生的错觉,而机器实际上是先运行这个程序一会儿,再运行那个,然后是第三个。并行是多出来的硬件所带来的现实,并发则一半是现实,一半是错觉。
实现同步原语很难,吉多在文章里写道,因为在有意识的层面上,我们的大脑并没有被训练成同时追踪多件事。你可以一边走路一边嚼口香糖;但如果一边核对账本,一边看悬疑剧,你就会把数字弄混,或者漏掉线索。以当前的技术水平,程序员仍然要负责把代码写对。要在脑子里记住一份顺序执行的菜谱已经够难了:a 里有什么,b 里有什么,c 里有什么。如果一段代码用 a 来表示渔夫,另一段代码用同一个 a 表示美人鱼,顺序执行时,可能一个用途在另一个开始之前就结束了。两者混在一起,美人鱼的计数就会覆盖渔夫的计数。你必须预判各种执行顺序。你以为自己很聪明,于是给它加了一把锁。以每千行 bug 来算,这个领域里的一切都更糟。锁像是单个烤箱门口的牌子:在使用中,或者空闲。信号量则像一块电子屏,显示还有五个烤箱空着,或者已经有三个人在排队,因此某个空闲的烤箱可能已经被留给了排在前面的人。
asyncio 在 3.4 版本前后进入标准库,此前有一对可追溯到 1990 年代末或 2000 年代初的旧模块已经停滞。大约在 2012 年或 2014 年,有人要求给那些模块加一个小功能,大家都说你不该用那些东西,于是为哪个第三方库才是正确的选择打起了一场架。吉多把这当作一个契机,做一个更好的标准库模块。那份 PEP 在当时是有史以来提出的最大的一份;他担任首席架构师,借鉴了现有库,并把成果放进了标准库。类型注解后来分散了他的精力,其他人让 asyncio 保持活力,它也很成功,尤其是在 Python 的 Web 客户端方面。无论在操作系统里还是在 Python 里,都存在两种架构阵营:Windows 偏向一种风格,Unix 偏向另一种。一种是事件驱动的:有数据包到达时通知我。另一种是为每个连接专门分配一个控制线程,阻塞在那个连接上。好几个传统的 Python 异步库用的是回调,就像把收到的纸张分门别类地放到一摞摞纸堆上,这种意大利面条式的风格吉多在审美上并不喜欢,而它在 JavaScript 的事件循环里很流行。他选择了基于任务的模型,复用 Python 已有的语言机制,并围绕这种范式构建了库。事实证明,这是个相当不错的选择。
GIL 是金发姑娘式的锁,Python 4.0 关乎的则是 C 扩展
全局解释器锁(GIL)臭名昭著,它解决的是一个从一开始就存在的问题:Python 的编写并没有考虑异步或并行。语言里没有并发,也没有线程。几年之后,SunOS、IRIX、POSIX 和 Windows 都长出了线程库。操作系统会处理线程;程序可以假装自己拥有和线程数一样多的 CPU,如果没有,操作系统会模拟。Python 当时不是为这个设计的。每家厂商都在添加略有不同的 C 线程 API。1990 年代初,线程看上去是很酷的新玩具,而 Python 的一个好处,就是给程序员提供了新的操作系统玩具的“安全”版本。套接字就是对 BSD 套接字的字面翻译,但它把 C 里悄无声息的挂起变成了清晰的 Python 错误。团队想对线程做同样的事,又不想为了线程安全而重写解释器,因为每个对象都假设只有一个线程存在。于是他们接受了损失:做出看起来像线程的东西。在单 CPU 的机器上,也就是那时大多数计算机的样子,它用起来确实像线程,因为即使一个 CPU 也能跑十来个线程。后来芯片设计者不再一味提高单核速度,转而在一块芯片上放多个核心,GIL 就变得臭名昭著。所有这些 Python 线程,依然只跑在一个核心上。
它能消失吗?吉多看到的最有可能的未来,是多个子解释器,每个子解释器运行一个完全独立的 Python 程序,它们之间有某种更快的通信方式,由运行时来管理。即便如此,程序之间的对话仍然要花更多时间。共享对象既是线程的魅力,也是它的致命伤:你会共享你原本不打算共享的东西,并发 bug 随之而来。这也是他写那篇信号量文章的原因。没有 GIL 的、自由线程的 Python 也会带来大量软件 bug。其实已经有一个可行的实验:一位 Facebook 工程师把 CPython 分叉成了一个“无 GIL”的解释器,做了足够多的优化,使单线程情况下不会慢太多,而多线程情况下可以用满所有核心。核心开发者面对的问题是:他们是否愿意无限期地维护这份复杂性,是否愿意接受单线程路径上的额外开销。吉多觉得,并没有足够多的人需要 Python 的多线程速度,值得为此承担这个代价。在他看来,GIL 是一个相当不错的“金发姑娘”式平衡点,介于完全没有线程和始终全部都是线程之间。并非所有人都同意他。子解释器看起来是 3.12 的一个较稳妥的选择,按每年一次的节奏,还有大约一年。
考虑到从 2 到 3 迁移时的痛苦、欢喜、煎熬与胜利,还会有 Python 4.0 吗?社区和核心团队对那次过渡记忆犹新,这也是大家都很高兴短期内不会有 4.0 的原因之一。如果真有,他们会把过渡规划得很不一样。他们当初低估了用户能够承受的程度。如果早知道,他们可以把 Python 3 设计得略有不同,同时不让它变得更差。Python 2 在 2020 年 1 月 1 日正式停止支持,对于剩下的用户来说是一个具有象征意义的时刻;而核心团队早在几年前就已经转向别处。他们原本计划在那一年的美国 Python 大会上办一场庆祝,但被疫情取消了。吉多心中的 4.0 设想并不是新的语法。如果他们真的发布了无 GIL 的 Python,对于建立在 C 扩展之上的那部分生态来说,它会有足够大的不同,而那部分生态,几乎就是整个机器学习、数据科学和科学计算的世界。假设在 3.19 之后,他们把一个版本命名为 4.0,并拨动那个开关。他希望那一年不添加任何新语法、新标准库模块或新内置函数。在 Python 层面,它应当与 3.19 兼容。扩展模块都需要重新编译,二进制接口会改变。纯 Python 用户会很轻松,不过现在纯 Python 用户已经很少了:所有做重要工作的人都依赖第三方扩展,PyPI 上有几十万个,其中有些已经缺少维护者。
比较人道的路线是提前多年发出预警。也许到了 3.15,无 GIL 会变成一个编译期选项:修改一个配置变量,重新构建,同时也要重新构建你需要的所有扩展。在那些扩展存在之前,这样做并不现实,但它让维护者可以先试水,找出将会消失的 API,并编写替代方案。接下来的一系列版本会逐步增强对无 GIL 的支持,而 4.0 则像是市长剪彩:无 GIL 成为默认,甚至可能是唯一的模式。被问到有哪些必不可少的 PyPI 库时,吉多没有现成的清单。他写 Python 时,通常是围绕构建 Python 本身的工具,而他们最不想要的就是第三方依赖,所以他大部分时间待在标准库里。如今缺少的部分,通常是新想法,而它们在第三方库中的实现演进得比标准库快。把它们纳入标准库,反而会让它们慢下来。他喜欢活跃的包生态。近期的标准库工作包括一些大扫除:他在 1992 或 1993 年草草写下的模块,提交历史只是在修饰外观,API 与二十年前一模一样,有时候替换成第三方的替代品反而更好。
Python 凭借“计算引导”的角色赢得科学界,其余的交给了开源
吉多觉得,Python 成为机器学习的主导语言(PyTorch、TensorFlow、scikit-learn,底层还有 NumPy、SciPy、pandas、matplotlib)这件事合理吗?他说,部分原因简单得就像大家都靠右行驶:兼容性。他们本可以选 JavaScript 或 Perl。2000 年代初,Perl 看起来真的要主宰生物科学,因为 DNA 搜索靠的是正则表达式,而 Perl 仍然拥有最快、最全面的引擎。任何东西都比用 C 或 C++ 去写面向科学家的那一层要好。一位曾在劳伦斯·利弗莫尔国家实验室计算部门工作的人,给吉多寄来了他的回忆录,讲自己怎样帮助“计算引导”(computational steering)这个概念诞生:把那些用 Fortran 写成、解决普遍数学问题的库拿过来,让不断提出新问题的科学家,用更高层的语言把这些库串起来。Paul DuBois 是 1990 年代中期看到这一需要的人之一。吉多第一次创造 Python 时,并没有预料到它会被用来处理数字数组。他以为数组是一种过时的数据类型;Python 擅长字符串和对象,语言里甚至不存在多维数组。后来人们意识到,这门语言的可扩展性足够灵活,可以写出高效处理大型数组的第三方包。
它们在科学界的不同领域各自站稳了脚跟。吉多记得,1990 年代末,巴尔的摩的哈勃太空望远镜团队是 Python 的狂热拥趸。一旦研究相似数据的科学家(问题不同,底层库相同)可以交换一个 Python 库,而不必从零开始用 Perl 或 JavaScript 去写,这门通用语言就会自我滋养。之后新出现的基础库,就会选择 Python,因为它们的用户已经会用 Python 了。他猜想,这就是 TensorFlow 最终用 Python 做用户界面的原因。莱克斯补充了竞争者的历史:谷歌内部的前身、Theano、Caffe、一些 Scala 方案,但 Python 已经占据主导。MATLAB 曾经是工程领域的替代方案,而 MathWorks 不把它开源,又按席位收费,大学很不喜欢,这一点很关键。吉多认为,大学还是会付钱的。更深层的失误在于,MATLAB 没有滋养 GitHub 上的开源包文化:那种爱折腾的文化,让你能从零开始做一个包,感到兴奋,然后分享出来。吉多喜欢的 Python 开源,几乎是平等的。你仍然要决定靠左还是靠右行驶,但你不必在一家大型科技公司工作,也能产生影响。Python 软件基金会(PSF)的大部分钱花在了促进社区的活动上,而不是开发上。
四年前,他刚刚卸下终身仁慈独裁者(BDFL)的职务时,两人谈过这个角色。回想起来,他可能坚持得太久了。他记得自己很长一段时间压力极大,却说不清压力从何而来,他觉得自己应该更早放下核心角色。对社区来说,好处是愿景的清晰。Python 壮大的过程中,他确实会受到影响,同时也坚持了某些原则。这些原则很难列成清单,但这个角色本身就示范了该如何思考问题。人们了解他是怎么想的,就能预测他会怎么做决定。没有那种这届当权者推翻上届、下届再推翻这届的折腾。接替他的指导委员会也找到了类似的方式:既能领导,又不停滞。对他个人来说,现在更有乐趣。多进程里出了个 bug?让别人去判断它重不重要。他可以专心处理类型标注、asyncio 和更快的解释器。
他工作过的几家公司,文化差异确实很大。Dropbox 规模小得多,产品更窄,代码库更小,在他任职期间,公司有个习惯:整个公司押在一份一年期计划上,然后发现一切都错了;每当新副总裁上任,就要来一次年度工程重组。莱克斯仍然很喜欢这款产品:同步毫不费力,冲突处理得当,是那种你不该去想它的服务。吉多说,公司内部有很多担忧和焦虑,但产品始终运行得很顺畅。他得出的教训是熟悉的:如果产品是好的,就继续做你擅长的事。微软在纳德拉(Satya Nadella)领导下的转变(收购 GitHub,拥抱开源和开发者),是吉多在一段短暂的退休之后又加入的原因。那段退休让他意识到,退休这个念头很荒唐;工作本身也可以带来成就感。他和 Excel 团队的人聊过。这个电子表格大约有 35 年历史,至今还能读取软盘上的文件。纳德拉他没见过,但听说他是个思维敏锐、商业感觉很强的人,把一家曾公开敌视开源的公司,变成了积极拥抱开源的公司。Excel 不会突然开源。但像 VS Code 这样的产品是有空间的,而它恰恰是开源的。
因为想做点什么而去学 Python;Copilot 负责敲字,决定权仍在程序员手里
吉多写过一篇博客,回答一位有计算机学位的人:是该去做朝九晚五的工作,还是去创业。莱克斯指出,创造 Python 本身就是一种创业行为,而优秀的程序员总是面对同样的岔路口。吉多的地图更宽广:你可以在大公司里创造新东西;如果它长大了,而你也有这样的抱负,就可以升任去管理它。你也可以做那个写出第一版、却完全不想管理一支从五人到一千人团队的天才工程师,然后去发明下一个疯狂的东西,或者创办公司,或者转身离开。德鲁·休斯顿(Drew Houston)起步时是技术发明者,吉多在 Dropbox 时,他仍是 CEO,并且一直在位,他具备成长并稳坐高位的技能。另一类人是出色的工程师,却很不擅长管理。吉多把自己归在后一类。他最初的爱好仍然是做一个个体贡献者,做一名程序员。
关于如何正确学习 Python 的建议:找一件你真正想用它做的事情。“我想学技能 X”这种动机不够强。挑一个问题,哪怕是个疯狂的、不现实的问题,它会逼你真正去学编程。不必雄心勃勃:一个小的社交媒体机器人,一个推特机器人,自动化你每天做的某件事,或者处理文件。机器学习是一个不动声色的入门途径。一个一小时的教程,就能让你用预训练模型做出一些酷炫的东西,在这个过程中,你学到了足够的导入、输入输出和函数调用,先爱上它,再慢慢补基础。你不能指望靠一个一小时的视频学会 Python。吉多一时想不起那个写了篇幽默文章、抨击“10 天学会 Python”广告的人的名字;文章的笑点是,目标应该是“10 年学会 Python”。他不同意那句口号,但同意其中的批评:五分钟的教程通常很糟糕。你一个小时就能学会一个 hook。背语法并不能让你成为程序员。那 10 年,是沿途的经验和痛苦。
编程已经在变化:很多代码都是从 Stack Overflow 复制再稍作修改。吉多并不想贬低这种方式,它确实很高效。接下来是下一步。他每天都用 GitHub Copilot。它替他写了很多代码,通常略有错误,但仍然帮他省了打字:有一行它生成得很完美,他只需改一个词。有时,如果他把函数名起得恰当,它会写出五到十行的函数体,而他的 Python 足够好,能很快发现问题。它变成了一个舞伴。它并不能替他省下很多思考。因为他是个糟糕的打字员,他感激的是那些击键,而不只是 VS Code 里那些老派的点号补全菜单。程序员的工作会被威胁吗?“程序员的工作会因为 Stack Overflow 的存在而受到威胁吗?我不这么认为。”它处理的是无聊的部分。你不该用它去做你无法理解的事。最好的用法,是问题本身就是“请提醒我这件事该怎么做”:这个方法叫 foo 还是 bar,是 builder、constructor 还是 factory,参数是什么。它是一位出色的助手。决定代码要做什么的创造性工作,仍然完全属于你。
十年、二十年、五十年、一百年后的 Python 会是什么样子?元宇宙、火星、人形机器人?它最终会变成一门遗留语言,扮演重要的角色,却是大多数人从没听说过、也不需要知道的东西,“就像生物学里各种基础结构一样,比如线粒体。”它渗透进数字生活,人们在它之上构建,却只知道上层。大多数程序员很少需要做二进制运算。吉多十几岁开始做起,用晶体管和电阻做成与非门,搭出小小的数字电路,他觉得自己的起步很幸运。他能往下走很远,比如讲到移位寄存器;至于怎样真正用硅制造芯片,在他看来仍然是魔法。他熟悉的是堆栈的中间层:到与非门以下,他就跟不上了;到了机器学习这一层,他也跟不上,它可能会搭出帮助我们理解物理的更高层,也可能搭不出。人脑做着巨量的并行工作,比如识别一张脸,然后利用这套电路,模拟出一个单线程的、更高层的解释器:一个念头接着一个念头的错觉。DNA 更像一颗你种下的种子,而不是你买来的电脑。细胞在复制,生物在复制,社会在自我维持。从外星人的视角看,那个值得研究的有机体,也许是地球上的生命,或者文明,盯着这一层看的个体只是其中的一部分。
同频道的其他文章
3:36:56EN → 简体中文
5:15:51EN → 简体中文
3:12:58EN → 简体中文
3:46:18EN → 简体中文
3:51:47EN → 简体中文
2:53:42EN → 简体中文