一行代码与三十多年
0:00新建一个空文件。
写下一行:print("你好")。
运行。
没有项目骨架,没有类型声明,也不需要先解释一整套运行模型。很多语言要到后面才给你的结果,Python 可以从这一行就开始给。
它当然不因此自动成为“最容易的语言”。但这种开始的方式很 Python:先让代码短,让结构看得见,让一个刚进来的人不用穿过太多形式,先得到一次回应。
[030]
引用 [030]
一行 print 是可直接复现的当下场景;Guido 约 1999 年的回顾支撑 1989 年底起点及可读性设计观点。不能据此把 Python 排名为最容易的语言,也不能把后见回顾当作 1989 年同期日志。
Foreword for Programming Python: December 1989 hobby project; paragraphs on indentation, readability, reduced visual clutter, and clarity inherited from ABC
反差在于,这门看上去总像新手入口的语言,并不年轻。
Guido van Rossum 后来回顾,Python 起于一九八九年十二月。到今天,它已经走过三十六年多。那篇回顾还专门解释了缩进和可读性:减少视觉噪音,让代码更短,也让不同人写出的程序更容易互相阅读。
“容易开始”不是今天替 Python 补上的广告词。至少在创建者自己的解释里,清晰和可读从很早就是设计的一部分。
[030]
引用 [030]
一行 print 是可直接复现的当下场景;Guido 约 1999 年的回顾支撑 1989 年底起点及可读性设计观点。不能据此把 Python 排名为最容易的语言,也不能把后见回顾当作 1989 年同期日志。
Foreword for Programming Python: December 1989 hobby project; paragraphs on indentation, readability, reduced visual clutter, and clarity inherited from ABC
也正因为这样,后来的那次选择才格外刺眼。
一门努力让代码易读、让人容易开始的语言,为什么会主动发布一个不能让大量旧代码原样运行的新版本?
答案不在一条新语法里。它藏在语言设计者、包维护者、应用团队和无数仍在运行的系统之间。
先把时间拨回一九八九年年底。那时没有 Python 2,也没有 Python 3,只有一台家用电脑和一个准备在圣诞周动手的解释器。
[065]
引用 [065]
这是从设计反差提出问题,不把提问写成已经证明的因果结论。
Python origin retrospective and PEP 3000 statement of deliberate Python 2 / 3 incompatibility
从业余解释器到可分发语言
2:04多年以后,Guido 把这个起点称为一个 hobby project。
他想写一门新的脚本语言:它是 ABC 的后代,但要能吸引 Unix 和 C 的使用者。ABC 很优雅,也面向非专业程序员;可在 Guido 的回顾里,它像一个封闭系统,很难加入新的 primitive,也很难自然接上 Unix 世界已经存在的工具。
Python 要保留清晰,却不能把自己封在清晰里面。它要允许扩展,借用 Unix 的基础设施和习惯,又不被单个平台绑住。Guido 还把 Modula-3 列为主要影响之一。
这些都是创建者约一九九九年回看起点时给出的解释。它们能告诉我们,他后来怎样理解 Python 的来源;不能把每个动机写成一九八九年当晚已经排好的清单。
[031]
引用 [031]
创建者后见前言支撑 hobby project、ABC 后代、扩展限制、Unix/C 受众与 Modula-3 影响;不能精确还原 1989 年逐步动机,也不能把回顾写成同期记录。
Foreword for Programming Python: December 1989 hobby project; descendant of ABC for Unix/C hackers; ABC closed-system limitation; Unix infrastructure; Modula-3 influence
几年后,这个解释器已经不只属于一台家用电脑。
一九九四年一月二十七日,Python 1.0.0 的发布邮件给出多个 FTP 镜像。Unix 使用者可以取得源码,Mac 和 DOS 也有各自的入口;随附的 NEWS 继续说明源码组织、文档、可移植性和 Mac port。
这还不是一个可以用下载量讲述的流行故事。现有资料只让我们确认:Python 已经公开分发,并且从早期就试图跨过不同平台。至于有多少人正在使用、哪一天突然普及,来源没有给出答案。
但语言一旦开始进入别人的机器,兼容就不再只是设计者和解释器之间的事。每一份脚本、每一个扩展、每一种平台习惯,都会在未来版本改变时成为下游。
[032]
引用 [032]
同期发布邮件与 NEWS 支撑公开分发、文档和跨平台入口;只能说明采用入口已经存在,不能证明用户数量、市场份额或单一爆红时刻。
Python 1.0.0 release email dated 1994-01-27: FTP mirrors and Unix, Mac, DOS access; Python 1.0.0 NEWS: source organization, documentation, portability, and Mac port
Python 最早解决的是“怎样让更多人可以加入”。
十多年后,它会碰到相反的问题:当已经加入的人足够多,一门语言还怎样改变那些无法在兼容边界里彻底修掉的旧规则?
[066]
引用 [066]
这是从公开分发过渡到下游兼容问题的叙事连接,不声称有早期用户规模数据。
Python 1.0 public distribution and PEP 3000 compatibility transition
核心团队为什么选择 break
4:01Python 并不是第一次改变旧行为。
二十一世纪初,除法已经示范过一条较慢的路。PEP 238 引入 //,又提供 __future__.division 和 warning,让项目可以先看到旧 / 语义会在哪里变化,再逐步采用新的规则。
这条路很长,也很具体。它说明核心团队知道兼容迁移需要预警和中间状态。但它不能证明,Python 3 的每一项变化都能用同样长的跑道解决。
[033]
引用 [033]
PEP 238 支撑除法变化的渐进迁移机制;它只是一项变化的路径,不能代表全部 Python 3 不兼容变化都采用相同策略。
PEP 238 Motivation, Transitional measures, Command Line Option, and API Changes: //, __future__.division, and warning-based migration
到二零零四到二零零六年,目标变大了。
PEP 3100 列出的不只是一个运算符,而是删除、重命名和模型变化。PEP 3000 更直接:Python 3 会破坏与 Python 2 的向后兼容。
这不是一次意外失手。核心团队主动接受了交换:让一部分旧代码不能原样运行,换取已经弃用的接口真正消失,让含混规则不再和新规则永远叠在一起。
这是设计者认为值得做的取舍,不是历史替他们盖章的唯一正确答案。清理范围很大,每项变化的必要性不同;新语言的长期收益,也不会自动替当时的使用者支付迁移成本。
[034]
引用 [034]
同期 PEP 支撑大范围清理与主动接受不兼容;关于长期收益和近期成本的分配是有边界的编辑判断,不能证明每项变化都必要或断裂是唯一方案。
PEP 3100 Introduction and change list; PEP 3000 Compatibility and Transition and Implementation
计划里并非没有过渡。
Python 2.6 可以发出 Py3k warnings;部分新能力可以 backport;2to3 可以转换源码;项目还可以暂时维护面向两个版本的发布产物。
这些工具把迁移责任安排得很清楚:上游提供方向和机械帮助,真正的数据语义、依赖和交付风险,仍由每个下游项目负责。
[035]
引用 [035]
PEP 3000 支撑最初过渡方案;它是过程设计,不证明工具能自动完成迁移,也不证明下游会按原先预期的速度采用。
PEP 3000 Transition Plan and Implementation: Py3k warnings, backports, 2to3, and two source distributions
为什么有些旧模型很难在兼容里修?先只看一个地方:str。
在 Python 2 里,str 既可能装人读的文字,也可能装网络或文件里的字节。同一个类型名下面,是两种需要不同处理的数据。程序平时可以含混地往前走,直到编码、协议或序列化把差异暴露出来。
Python 3 把文本和 bytes 分开。它要求程序明确:这里是 Unicode 文字,还是编码后的原始数据。
核心开发者 Brett Cannon 在二零一五年回看这段历史时,把这种混合视为 Python 3 存在的重要原因。他也承认,团队原先以为社区会更快跟上。那是一个核心参与者的后见解释,不是所有人的共同口供。
[036]
引用 [036]
版本文档固定文本 / 字节模型变化,Brett Cannon 以后见观点解释动机并承认采用预期偏差;不能把他的解释代表全体核心开发者,也不能把 Unicode 写成全部断裂的唯一原因。
Why Python 3 exists: Python 2 str as text and bytes, adoption expectation, and future incompatibility reflection; What's New In Python 3.0: Text Vs. Data Instead Of Unicode Vs. 8-bit
文本和字节只是显微镜,不是整份判决书。
Python 3 还把 print 变成函数,改变整数除法,让一些过去直接返回列表的接口改为视图或迭代器。它们触碰的是不同的旧行为,也给不同代码带来不同代价。
把这些变化全部说成“为了 Unicode”,会让故事显得干净,却不准确。核心团队选择的是一次集中清理;文本和字节只是最能看见设计收益与迁移痛苦同时存在的切口。
[037]
引用 [037]
版本文档支撑 print、迭代器 / 视图、整数和文本 / 字节等用户可见变化;不能推断每项变化具有同样必要性、成本或动机。
What's New In Python 3.0: Print Is A Function, Views And Iterators Instead Of Lists, Integers, and Text Vs. Data; PEP 238 division model
二零零八年十二月三日,Python 3.0 发布。二零一零年七月三日,Python 2.7 发布。又过一年,PEP 404 把 Python 2 的下一条 feature line 写死:不会有 Python 2.8。
这三个动作关闭了上游继续发展 Python 2 的方向,却没有替任何应用改完代码。解释器已经抵达未来,使用者还要自己回答:文件、网络、数据库和依赖递来的每一个字符串,究竟是什么。
[064]
引用 [064]
三份发布 / 路线文档固定 Python 3.0、Python 2.7 和不发布 Python 2.8 的顺序;它们不能把发布写成生态采用,也不能证明下游已有完整迁移路径。
PEP 361 Python 2.6 and 3.0 release schedule; PEP 373 Python 2.7 release and maintenance schedule; PEP 404 Abstract and Un-release Schedule
普通开发者怎样过河
7:29对一个普通开发者,最诱人的想法是:既然差异写得出来,就让工具一次改完。
2to3 确实能移动很多表面的东西。它可以改写 print,调整导入,替换一批已经改名的接口。可它看不见数据从哪里来,也不知道一个 str 是用户名、UTF-8 编码后的正文,还是协议报文。
转换器可以改语法,不能替应用理解自己的数据流。真正的迁移单位于是变小了:改一部分,运行测试;把 warning 变成清单;在文件、网络和序列化边界逐处决定 encode 还是 decode。
[038]
引用 [038]
过渡规范和版本文档支撑机械转换与文本 / 字节人工审计的边界;不能据此说工具毫无价值,也不能假定所有项目具有相同数据流。
PEP 3000 Transition Plan and 2to3 limits; What's New In Python 3.0 Porting To Python 3.0 and Text Vs. Data
想象一个很普通的迁移提交。第一步不是把整个仓库交给转换器,而是在 Python 2.6 上打开 Py3k warnings,让旧写法先暴露出来。第二步把 print、导入和已经改名的 API 交给 2to3。第三步才是最慢的人工工作:一个从 socket 读来的值,要不要在进入业务层时解码;一个要写回磁盘的字段,应该保留 bytes,还是先转成文本。
同一段代码在 Python 2 里可能一直没有出错,只因为默认编码、测试数据和生产数据碰巧相容。迁移把这个偶然性推到台面上。测试不是为了证明转换器聪明,而是为了让开发者知道,哪一个边界被自己重新定义了。
这也解释了为什么“普通开发者”不是一个轻松的角色。他没有核心团队的权限去改变语言,也没有包维护者的余力去替所有下游设计兼容层。他只能把一次大版本迁移拆成许多能够回滚的小提交,再用自己的数据和依赖证明每一步没有改变产品含义。
[076]
引用 [076]
这是把官方迁移步骤还原为可听的工作台;来源支撑工具和语义边界,不支撑某个未登记的真实开发者个案。
PEP 3000 transition plan and Python 3.0 porting guidance: Py3k warnings, 2to3, and text / bytes migration decisions
到二零一三年,一个叫 Python-Future 的项目把这种工作方式做成两阶段。
Stage 1 先做相对安全的现代化改写,尽量不立刻引入新的运行时依赖。Stage 2 再把代码推向 Python 3 语义,并要求开发者处理更难的兼容差异。代码、文档和 PyPI 记录把这套两阶段流程固定在 0.8.0。
它没有消除断裂。它改变的是一次提交要承担多大的未知:先让 diff 可以审查,让测试跟上,再进入需要人作判断的部分。
[039]
引用 [039]
版本固定源码、文档和元数据支撑两阶段流程随 0.8.0 形成;不能把它回填到 2008 年,也不能证明实际采用范围。
Python-Future commits e8ead804 and c67154db for --stage1 / --stage2 code and docs; PyPI future 0.8.0 upload metadata dated 2013-10-28
Stage 1 和 Stage 2 的价值,不只是把命令拆成两个名字。它们把“我要迁移”变成一个可以交给同事检查的顺序:先让旧程序更接近未来,再把依赖和运行时行为推过去。每一阶段都可以运行测试、看失败、停下来修复,而不是等到最后才面对一个无法定位的巨大差异。
但这条路也带着早期设计的痕迹。原始过渡计划允许项目分别维护 Python 2 和 Python 3 的发布产物;共同源码、兼容层和两套测试后来才逐渐成为更现实的组合。迁移工具提供的是脚手架,项目仍然要决定自己愿意维护几份源码、支持几种解释器,以及什么时候把旧产物冻结。
[077]
引用 [077]
两阶段的协作和发布含义由过渡计划与 Python-Future 资料共同支撑;不能声称所有项目都按同一路线迁移。
- Guido van Rossum,PEP 3000;2006-04-05
- Python Charmers / Ed Schofield,`futurize` 两阶段实现、文档与 0.8.0 发布;2013-10-23 至 10-28
PEP 3000 two-distribution transition and Python-Future Stage 1 / Stage 2 workflow
然后,时间走到二零一四年一月二十八日。
Python-Future 0.11.0 的 Quick-start 先给出一条命令:pip install future。
接下来的承诺更重要。项目希望你写出更接近 Python 3 的代码,同时仍让同一份源码运行在 Python 2;它还把从 Python 3 往回补兼容的 pasteurize 独立出来。
“安装未来”到这里才出现。它不是一句开场文字游戏,而是多年断裂之后,一个普通项目终于可以执行的历史动作:先装上一层兼容,再把迁移切成能够提交、测试和暂停的步骤。
可工具仍然不会回答那个最难的问题。它可以给你一个新的 str,不能告诉你这个位置本来应该保存文字,还是字节。
[040]
引用 [040]
版本固定文档与元数据支撑命令、双版本承诺和 pasteurize 分离;不能把当前 Quick-start 全部内容归入 0.11,也不能把工具写成自动解决文本 / 字节语义。
Python-Future v0.11 quickstart: pip install future and Python 2/3 single-source promise; v0.11 whatsnew: pasteurize separated from futurize --from3; PyPI 0.11.0 upload metadata dated 2014-01-28
真正的安装动作因此很小,后续责任却很大。future 可以把 Python 3 风格的 builtins 和兼容实现带回 Python 2,也可以把一部分语法变化变成可复用的库代码;它不能替你检查数据库里的旧编码,不能替你确认第三方库返回的是文本还是 bytes,更不能替你决定一个公共 API 是否已经可以改变。
普通项目需要同时看三张表:源码表,记录哪些改写已经完成;运行时表,记录 Python 2 和 Python 3 各自通过了哪些测试;依赖表,记录哪些外部包还没有给出兼容版本。只有三张表都能持续更新,pip install future 才不是一次性的安装成功,而是迁移工程的起点。
[078]
引用 [078]
把工具能力与项目责任分开;来源不证明 future 自动解决数据库、第三方 API 或公共接口的语义。
- Python Charmers / Python-Future,0.11 Quick-start、changelog 与 PyPI 元数据;2014-01-28
- Python Charmers / Ed Schofield,`futurize` 两阶段实现、文档与 0.8.0 发布;2013-10-23 至 10-28
Python-Future v0.11 quickstart and two-stage documentation: compatibility builtins, pasteurize / futurize, and single-source Python 2 / 3 workflow
一个应用团队可以决定今天改哪一个文件。
一个包维护者不只面对自己的代码。它发布的新版本会进入成百上千个下游;这些下游有人已经转到 Python 3,有人仍在 Python 2,还有人依赖的别的包根本没有动。
同一座桥,到了维护者脚下,会承受来自两个方向的重量。
[067]
引用 [067]
这是从应用迁移过渡到包维护者责任的叙事连接,不把下游数量写成可核验规模。
- Python Charmers / Python-Future,0.11 Quick-start、changelog 与 PyPI 元数据;2014-01-28
- Jannis Leidel,Django Developers `Python 3 and you`;2011-09-14
Python-Future single-source promise and Django's dependency / user-application migration layers
包维护者站在桥中间
12:31二零一一年,Django 的开发者讨论把迁移拆成三层。
第一层是 Django 自己。第二层是它依赖的第三方库。第三层是建立在 Django 上的用户应用。框架即使先完成,依赖或应用没有路,使用者仍然过不去。
讨论里有人明确反对把未来做成一个 Python 3-only 分支,也反对长期维护两条独立 release line。前者可能把仍在 Python 2 的社区留在原地,后者则把每个修复都变成双份维护。
这是一封同期维护者讨论能证明的立场,不是已经执行完的迁移,也不表示每位参与者完全同意。
[042]
引用 [042]
同期邮件支撑三层迁移对象及维护者对分支策略的观点;讨论不等于迁移完成,也不能代表全体成员形成完全一致意见。
Django Developers Python 3 and you email body: Django, third-party dependencies, and user projects; rejected Python 3-only and separate dual-release branches; combined support discussion
一年后,Django 宣布 Python 3 支持进入 experimental 状态。
核心团队采用 six,让代码更接近 Python 3 的写法,同时继续运行在 Python 2;他们也重命名涉及 string 和 unicode 的接口,让数据含义在名称上更明确。
但项目没有把“测试通过”写成“迁移完成”。公告承认,真实使用中的测试还不够。框架自己的 test suite 可以证明一部分兼容,不能替所有第三方应用和部署方式发言。
[043]
引用 [043]
同期项目公告支撑 six、共同源码、API 重命名与真实使用测试不足;不能写成所有 Django 应用已兼容,也不能外推所有框架。
Django Experimental Python 3 support: six, Python 3 code made compatible with Python 2, string / unicode API renames, and insufficient real-world testing
这把维护者的难题从代码库里推了出去。Django 可以在自己的测试矩阵里确认某个 API 名称已经更清楚,却无法替一个用户应用确认它有没有把 Unicode 当作数据库连接的 bytes,也无法替一个第三方库保证它在两个解释器里都能安装。框架先提供一条共同源码的路,真正的采用仍要沿着依赖和应用一层层向下发生。
因此,Django 的 experimental 不是“Python 3 已经支持 Django”的终点,而是一个公开的中间状态:核心项目开始承担新语义,外部使用者仍被邀请带着自己的应用回来测试。这个状态比开一条完全分裂的 Python 3 分支更容易共享修复,却也把未覆盖的下游风险保留在现场。
[079]
引用 [079]
Django 材料支撑维护边界和测试不足;这里不虚构具体下游应用或迁移结果。
- Django,`Experimental Python 3 support`;2012-08-19
- Jannis Leidel,Django Developers `Python 3 and you`;2011-09-14
Django 2011 dependency / user-application layers and 2012 experimental support announcement with insufficient real-world testing
Jinja2 的维护者遇到的是另一种摩擦。
早期方案会在安装时运行 2to3,生成 Python 3 版本。理论上,维护者只写一份 Python 2 源码;实际上,每次修改后都要等待转换,也要担心生成结果被悄悄改坏。Armin Ronacher 在二零一三年的复盘里,把这种等待和心理负担写得很具体。
他转向 python-modernize 和共同源码,让同一个代码库在 Python 2.6、2.7 和 3.3 上持续测试。变化不是“找到更聪明的一键迁移”,而是停止生成两份容易分叉的结果,把兼容重新放进日常测试循环。
[041]
引用 [041]
Jinja2 维护者复盘支撑安装时转换的迭代成本与统一源码路径;只代表 Jinja2,具体耗时、心理感受和工具选择不能外推整个生态。
Porting to Python 3 Redux: installation-time 2to3 iteration cost, fear of breaking generated Python 3 support, python-modernize, single-source code, tox, and Python 2.6 / 2.7 / 3.3 testing
这项选择的代价也很具体:Jinja2 不再试图覆盖所有旧版本,而是把支持范围收窄到一个可以持续测试的交集。维护者每天面对的不是“转换器有没有成功运行”,而是某个模板、某个 Unicode 边界和某个第三方 API 在多套解释器下是否仍然有同一行为。兼容性从安装时的一次魔法,变成每次提交都要支付的测试成本。
Jinja2 的经验和 Django 不一样。Django 试图先守住框架、依赖和用户应用之间的共同路线;Jinja2 先解决的是生成代码拖慢维护的问题。它们都没有取消 Python 3 的断裂,只是把成本放到了不同位置:一个留住更大的下游交集,一个缩小自己的版本承诺。
[080]
引用 [080]
Jinja2 的版本交集和维护体验来自维护者复盘,只代表该项目,不外推整个包生态。
Jinja2 porting retrospective: narrowed Python support intersection, python-modernize, single-source testing, and compatibility maintenance cost
共同源码解决“怎样同时维护”,却迟早会遇到“何时不再同时维护”。
二零一七年,NumPy 的 NEP 14 给出一条明确边界:从二零一九年开始,新的 feature release 只支持 Python 3。仍在 Python 2 的使用者可以继续从 PyPI 安装最后的兼容版本,只是不再得到后续新功能。
这不是删除过去。它把过去放到一条不再向前扩展的版本线上。对维护者来说,停止双版本支持是有限资源的分配;对下游来说,等待的代价从“以后再迁”变成“以后只能停在旧版本”。
NumPy 不能代表整个科学计算生态。但它让包维护者的终点第一次变得很清楚:兼容可以是一座桥,不能自动成为永久义务。
[044]
引用 [044]
NumPy 政策支撑 feature release 退出、旧版仍可安装及维护资源观点;不能写成科学计算生态同日完成迁移,也不能说旧 Python 2 版本消失。
NEP 14 Abstract, Detailed description, and Implementation: 2019 Python 3-only feature releases, continued PyPI availability of old Python 2-compatible versions, and limited maintenance resources
对一个依赖 NumPy 的项目,这个公告改变的不是今天能不能安装,而是下一次升级会不会带来新能力。继续留在 Python 2 的项目可以把最后的兼容版本锁住,维持一段可运行的环境;但它也同时接受了一个交换:新的 feature release、新的修复和新的工具链不会再沿这条旧线继续增长。
这是一种比“删除旧版本”更慢、也更清楚的退出。维护者不用在某一天让所有下游同时失败,下游也不能再把“上游还没有关门”理解成“上游会永远替我维护”。版本号、PyPI 上仍可安装的旧包和新 feature line,共同把停止支持变成一个可以被依赖管理器看见的边界。
[081]
引用 [081]
NumPy 材料支撑版本边界和旧版可安装状态;不推出整个科学计算生态同步退出。
NumPy NEP 14: Python 3-only feature releases from 2019, last Python 2-compatible line remaining installable, and finite maintenance boundary
包维护者可以选择下一版支持谁。
大型系统没有这么轻。一个桌面客户端要保护真实用户,一组云基础设施项目还要保护稳定分支、发行版和彼此之间的依赖顺序。
要看清这种差别,下一章把专题时间拨回二零一四年。先看一张没有走通的依赖图,再看一个团队怎样把两套解释器真的装进同一个产品。
[068]
引用 [068]
这是从包维护转入产品与多项目系统的叙事连接;两个案例保持各自边界。
Dropbox migration retrospective and OpenStack Python 3 sprint dependency list
一个产品与一张依赖图
17:57先看一个产品团队怎样发现“一次转换”不够用。
Dropbox 的迁移复盘把时间拨回二零一五年。一次 Hack Week 试验让客户端可以登录、同步一部分文件,但大量功能仍然损坏,许多工作被丢弃。这个结果不是“开发者不愿意迁移”,而是应用规模、依赖和数据边界把错误一起放大了。
到二零一七年,旧工具链带来的压力让 Python 3 迁移获得正式资源。团队升级或 fork 依赖,把 Python 3 测试和 Mypy 放进持续集成,再逐文件缩小例外。序列化边界还会出现 str(bytes) 这样的错误:程序表面上跑起来了,写出去的数据却已经不是原来的意义。
这里最重要的不是“Dropbox 有足够多工程师”,而是失败第一次被拆成了可以领取的任务。某个依赖没有 Python 3 版本,就升级、fork,或者明确把它列为阻塞;某个测试暂时跳过,就不能让跳过本身变成绿灯,而要逐文件减少例外;某个类型检查黑名单过大,就要让它随着迁移逐步缩小。迁移从一句组织口号,变成 CI 里不断减少的未知数。
但这些工具只能发现错误,不能替团队做产品决定。Mypy 可以提醒某个值的类型不稳定,测试可以重现一次序列化变化,最后仍要由熟悉协议和旧数据的人决定:是修正代码,还是保留旧格式并在边界显式转换。
[045]
引用 [045]
参与者后见复盘支撑失败试验和正式资源的时间关系;不能把 2015 写成完整多年计划,也不能声称已经伤害真实用户。
Dropbox incremental migration retrospective: 2015 Hack Week result, damaged functionality, discarded work, and 2017 allocation of migration resources
Dropbox 的第一次失败因此没有被剪掉,而是成为后续工程的入口。它让团队知道,客户端迁移不是把解释器版本改一行,而是要同时整理依赖、构建系统、测试覆盖、类型约束和数据协议。
[082]
引用 [082]
Dropbox 复盘支撑工程任务的拆解;不能把这些工具说成自动完成语义迁移。
Dropbox incremental migration retrospective: dependency upgrades or forks, CI tests, Mypy blacklist reduction, skipped tests, and serialization failures
这也是为什么类型检查和测试不是迁移的替代品。它们能把错误更早变成可见的失败,却不能替团队决定数据库字段、文件格式和网络协议应该保存文字还是字节。
Dropbox 最后采用了更像产品发布策略的中间状态:Hydra 携带 Python 2 和 Python 3 两套解释器,在真正加载 Python 代码以前选择运行时;远程 gate 让团队可以逐步扩大 Python 3 的安装范围,也可以退回旧路径。
团队自报七个月把安装逐渐推到 Python 3,并在桌面客户端版本 52 移除 Python 2。这个完成点只属于 Dropbox 桌面客户端,不能外推到 Dropbox 全部系统,更不能当成所有企业的平均迁移周期。
Hydra 解决的是一个产品问题:用户不应该因为某个新解释器路径还没有被证明,就一次性承担全部风险。客户端先把两套解释器一起带上,在加载应用代码前选择其中一套;远程 gate 再把选择权从一次安装扩展到一批用户。于是失败可以被定位到某个运行时、某个依赖或某个渠道,而不是被包装成一次不可逆的全量发布。
这种回退不是迁移的反面。正因为团队可以在 Python 3 暴露问题时退回 Python 2,才有机会把新路径交给更多真实安装,而不是等到理论上“所有问题都解决”才第一次面对用户。代价是两套解释器、两套构建和一段时间的混合源码都必须继续维护。
[046]
引用 [046]
工程复盘支撑依赖、测试、类型检查和逐文件例外;不能把 Mypy 或测试写成自动完成迁移。
Dropbox incremental migration retrospective: dependency upgrades or forks, Python 3 tests, Mypy, and file-by-file exceptions
[047]
引用 [047]
具体错误来自 Dropbox 的桌面客户端复盘;不能外推成所有项目最常见的错误。
Dropbox incremental migration retrospective: str(bytes) and serialization / text-byte failures
[048]
引用 [048]
两份 Dropbox 复盘支撑产品级双解释器和灰度回退;不能把 Hydra 写成通用迁移工具,也不能外推到 Dropbox 全部系统。
Dropbox rollout retrospective: Hydra dual interpreters, pre-import interpreter selection, remote gate and rollback
[049]
引用 [049]
完成时间和范围均归属于 Dropbox 桌面客户端参与者复盘;不能外推为所有企业或 Dropbox 全部系统的完成日期。
Dropbox rollout retrospective: seven-month rollout and desktop client version 52 removal of Python 2
七个月之后,版本 52 移除的是桌面客户端里的 Python 2 路径,不是全世界的 Python 2。这个边界很小,却很重要:它让“迁移完成”变成一个可以验证的产品断言,也防止我们把一个团队的回顾误写成整个企业或整个生态的终点。
[083]
引用 [083]
两份 Dropbox 复盘支撑回退契约和产品完成边界;不能外推为所有企业的通用迁移方法。
Dropbox Hydra dual-interpreter selection before import, remote rollout gate, rollback, seven-month rollout, and desktop client version 52 removal
OpenStack 不能只复制 Dropbox 的办法。
二零一四年的 Python 3 sprint 页面已经把问题列成依赖图:自有组件之外,还有 MySQL-python、boto、Eventlet 等外部依赖。一个服务可以先改完,不代表它依赖的库、测试工具、发行版和稳定分支都已经准备好。
二零一八年,Technical Committee 把退出时间变成共同协议:各项目不能各自选择日期,否则下游发行版和独立部署者可能需要同时打包两套依赖。Stein 先把 python3-first 写进文档、lint、构建、发布和测试,再到 Ussuri 按服务、公共库 / 测试工具、requirements 分三阶段移除 Python 2。
最后的复盘仍保留边界:Swift、Storlets 和 Tempest 的稳定分支并不会因为主线切换就同时变得无影响。对 OpenStack 来说,“完成”是依赖顺序、CI、治理和例外共同组成的过程,不是一个仓库在某天改完 shebang。
OpenStack 的难处在于,它不能把回退开关只放在一个产品入口。一个服务的 requirements 可能由公共库提供,一个公共库又可能被多个服务和发行版打包;稳定分支还必须继续服务已经部署的用户。于是“先迁移谁”不是技术团队内部的偏好,而是治理层必须公开协调的顺序。
Stein 的 python3-first 把这种顺序写进了日常检查:文档、lint、构建、发布和测试都先以 Python 3 为默认。到 Ussuri,退出又被拆成服务、公共库和测试工具、requirements 三个阶段。每一阶段都减少一类旧依赖,同时留下对稳定分支和特殊组件的解释空间。
[050]
引用 [050]
五份材料支撑多项目依赖、共同退出协议、完成口径和例外;不能写成每个仓库同日完成,也不能外推所有开源项目群。
- OpenStack Wiki,`Python3/SprintPycon2014` 固定修订;2014-04-14 至 17
- OpenStack Technical Committee,`2018-05-29 Python2 Deprecation Timeline`
- OpenStack Technical Committee,Stein goal `Run under Python 3 by default`;2018
- OpenStack Technical Committee,Ussuri goal `Drop Python 2.7 Support`;2019–2020
- Ghanshyam Mann,`OpenStack Ussuri is Python3-Only: Upgrade Impact`;2020-05-27
OpenStack 2014 sprint dependency list; 2018 coordinated deprecation timeline; Stein python3-first criteria; Ussuri three-phase removal; 2020 upgrade-impact exceptions
这条路径没有 Dropbox 那种单次启动时的全局回退,却提供了另一种可控性:让每个项目知道自己必须先等谁、何时必须停止提供 Python 2 入口、哪些例外需要被记录。对一个多项目社区,截止日期真正购买的是协调时间,而不是让所有仓库在同一天神奇地变绿。
[084]
引用 [084]
OpenStack 材料支撑依赖顺序与治理动作;不能写成所有项目或分支同日完成。
- OpenStack Wiki,`Python3/SprintPycon2014` 固定修订;2014-04-14 至 17
- OpenStack Technical Committee,`2018-05-29 Python2 Deprecation Timeline`
- OpenStack Technical Committee,Stein goal `Run under Python 3 by default`;2018
- OpenStack Technical Committee,Ussuri goal `Drop Python 2.7 Support`;2019–2020
- Ghanshyam Mann,`OpenStack Ussuri is Python3-Only: Upgrade Impact`;2020-05-27
OpenStack dependency blockers, coordinated timeline, Stein python3-first criteria, Ussuri staged removal, and stable-branch exceptions
妥协不是一个动作
23:54二零一四年,Python 2.7 的支持终点从二零一五年延到二零二零年。
官方材料给出的政策目的,是缓解那些还不能及时迁移的用户和供应商的担忧。这个决定确实延长了共存期,却没有回答一个更强的问题:它是否让具体项目因此推迟?我们登记的同期讨论和后见说明,都没有找到某个库、企业或发行版明确说“因为延期,所以我们延后迁移”。
所以节目只能说延期提供了迁移时间和一个共同日期,不能把时间上的相关性讲成已经证实的拖延因果。
[051]
引用 [051]
PEP 固定政策日期与目的;不能推出延期导致任何具体项目拖延。
PEP 373 post-history: Python 2.7 end-of-life moved from 2015 to 2020 and rationale around users and vendors unable to migrate
[052]
引用 [052]
一封同期邮件与一篇后见新闻稿分别支撑当时责任边界的不确定性和后见政策解释;不能证明延期没有降低压力,也不能证明必然有益或有害。
- Senthil Kumaran,python-dev `death to 2.7; long live 2.7` 回帖;2014-04-10
- Python Software Foundation,`Python 2 Series to be Retired by April 2020`;2019-12-20
2014 python-dev discussion on the meaning of Python 2.7 maintenance; PSF 2019 retrospective on migration, vendors and Unicode time
这次延期购买的是不同人的不同时间。仍在维护旧系统的团队得到更长的安全和供应商准备窗口,发行版和基础设施得到一个可以共同引用的日期,核心项目则暂时避免让所有未迁移用户同时掉出支持范围。它没有把 Python 2 重新变成有未来的 feature line,只是让“未来必须迁移”和“今天还不能迁移”可以同时成立。
这也是妥协最容易被误读的地方。延长维护并不等于认可旧语义,缩短新功能也不等于把旧用户赶出软件。它们都是在问:哪一部分能力必须回到旧版本,哪一部分成本必须由仍然使用旧版本的人自己承担。
[094]
引用 [094]
延期的多方时间边界是编辑性解释;资料不证明具体项目因此拖延。
- Benjamin Peterson,PEP 373;2008,2014 更新
- Senthil Kumaran,python-dev `death to 2.7; long live 2.7` 回帖;2014-04-10
Python 2.7 EOL extension rationale and 2014 maintenance discussion
如果没有某个项目自己的邮件或复盘明确把延期列为拖延原因,我们就不能替它补上心理动机。节目可以讲政策改变了可用时间和共同期限,但不能把“有了五年”写成“因此大家晚了五年”。
[085]
引用 [085]
资料支撑延期的政策边界和不确定因果;不能把相关性写成具体项目的已证实拖延。
- Benjamin Peterson,PEP 373;2008,2014 更新
- Senthil Kumaran,python-dev `death to 2.7; long live 2.7` 回帖;2014-04-10
- Python Software Foundation,`Python 2 Series to be Retired by April 2020`;2019-12-20
PEP 373 EOL extension, 2014 python-dev discussion, and PSF migration retrospective
另一种妥协不是把终点推远,而是把未来的一小部分带回过去。
PEP 414 让 Python 3.3 恢复 u"",扩大 Unicode-aware 代码在两个版本之间的共同源码。PEP 466 又把 Python 3.4 的网络安全能力回移到 Python 2.7.9,因为当时大型应用、发行版和基础设施不能只收到一句“请迁移”的建议。
six 和 future 把兼容层放进应用和包;双解释器则把同一套产品的切换推迟到能够灰度的时候。它们都不是取消 Python 3 的设计,而是把一次断裂拆成不同主体可以承受的几笔账。
[053]
引用 [053]
PEP 414 支撑共同源码交集扩大;不能写成恢复 Python 2 的文本模型。
PEP 414 rationale and specification: restoration of the u prefix in Python 3.3 for Unicode-aware Python 2 code
[054]
引用 [054]
PEP 466 支撑安全能力回移与替代方案被否决;不能把它扩大为所有 Python 3 功能都应回移。
PEP 466 rationale and resolution: network security backports to Python 2.7.9 and rejection of migration advice as the only response
PEP 414 的 u"" 是一种语法层面的让步:让已经写成 Unicode-aware 形式的代码在 Python 3.3 和 Python 2 之间保留更大的共同源码。PEP 466 则是另一种让步:网络安全能力不能等到所有大型应用迁移完才抵达 Python 2 的生产环境,因此一部分能力必须回移到 2.7.9。
两者都说明“兼容”不是一个抽象的美德,而是要回答具体问题。共同源码解决的是维护者如何少写一份分支,安全 backport 解决的是旧运行时今天必须得到什么保护。它们没有把 Python 2 变成 Python 3,也没有让文本、字节和第三方依赖自动消失;它们只是让迁移不必以安全风险或整套代码分叉作为前提。
[095]
引用 [095]
分别支撑共同源码与安全回移的具体目的;不扩大为所有功能都应 backport。
PEP 414 Unicode literal compatibility and PEP 466 security backport rationale
所以 six、future、u""、安全 backport 和双解释器不能被剪成一个“社区发明了兼容包”的金句。它们分属语法、库、网络安全和产品发布几个层面。只有把每个让步放回它要解决的阻力里,听众才会知道这条桥为什么越修越长。
[086]
引用 [086]
分别解释语法、网络安全和库层妥协;不能把任何一个 backport 扩大为全部 Python 3 能力的回移。
- Armin Ronacher、Alyssa Coghlan,PEP 414;2012-02-15
- Alyssa Coghlan,PEP 466;2014-03-23
- Python Charmers / Python-Future,0.11 Quick-start、changelog 与 PyPI 元数据;2014-01-28
PEP 414 u-string compatibility, PEP 466 security backports, and Python-Future compatibility layers
上游结束支持,也不意味着每个产品在同一天关掉旧运行时。
AWS Lambda 在 Python 2.7 上继续提供一段限定的 runtime 与 SDK 安全更新。Maya 2022.2 默认使用 Python 3,但在 Windows 和 Linux 上仍保留 Python 2 模式,macOS 则有不同边界。
这些例子不是在证明 Python 2 仍是通用主流,而是在提醒“迁移完成”必须说明是谁完成:CPython 上游、主流包工具链、多数活跃项目和商业嵌入产品,不是同一个主体。
AWS Lambda 的边界是托管服务:供应商可以继续替一部分既有函数提供限定的 runtime 和 SDK 安全维护,但函数依赖的第三方包、用户自己的部署流程和其他平台仍然是另一笔账。Maya 的边界则更像宿主软件:插件和脚本跟着桌面产品的系统支持矩阵走,Windows、Linux 和 macOS 甚至不必在同一天拥有同样的解释器入口。
这两个案例不能告诉我们“Python 2 还有多少用户”,也不能告诉我们供应商为什么选择这个日期。它们只把上游时间线切开:语言项目可以宣布 EOL,产品公司仍可以在自己的边界内延长一段有限支持;有限支持结束之后,用户还要面对自己的插件、依赖和部署环境。
[055]
引用 [055]
两个供应商材料只支撑限定产品的延后支持和双运行时边界;不能推出 2020 后 Python 2 仍是通用主流,也不能推断供应商动机或规模。
- Eric Johnson / AWS Lambda,`Continued support for Python 2.7 on AWS Lambda`;2020-01-02,更新 2020-10-20
- Autodesk,Maya 2022.2 `Migrating to Python 3`
AWS Lambda continued Python 2.7 runtime and SDK support announcement; Maya 2022.2 Python 2 / 3 runtime platform matrix
换句话说,生态接受 Python 3 以后,旧运行时还可能作为产品功能存在。但它不再享有公共生态同样密集的更新,这正是“仍能运行”和“仍有共同未来”之间的差别。
[087]
引用 [087]
供应商材料支撑产品边界差异;不支撑用户规模、供应商动机或后续终止日期。
- Eric Johnson / AWS Lambda,`Continued support for Python 2.7 on AWS Lambda`;2020-01-02,更新 2020-10-20
- Autodesk,Maya 2022.2 `Migrating to Python 3`
AWS Lambda Python 2.7 runtime / SDK support boundary and Maya 2022.2 platform-specific Python 2 / 3 matrix
生态怎样改变默认选项
29:03二零一五年的数字很容易被讲成一场胜负,但它们其实在测不同的东西。
PyPI 请求日志分析看到的是下载端:当时 Python 3 请求占比仍然很低。Brett Cannon 同期按 classifier、活跃度和下载门槛观察包供给,却看到活跃项目对 Python 3 的支持明显好于总下载环境。二零一六年另一项按包版本发布活动做的分析,又把 Python 3 支持超过 Python 2 当作趋势预测。
这些比例不能合并成一个“Python 3 采用率”。下载、classifier 和发布次数各自会受到镜像、CI、缺失元数据、高频发布项目等因素影响。但三份材料共同指向一个更稳的判断:活跃库的供给改善,早于大规模使用者的消费迁移。
[056]
引用 [056]
两份分析支撑消费端与活跃库供给的不同信号;不能合并为统一采用率,也不能把 Python 3.5 写成唯一拐点。
- Donald Stufft,`A Year of PyPI Downloads`;2015-04-20
- Brett Cannon,`Python 3 support on PyPI`;2015-06-12 / 13
2015 PyPI request-log analysis and Brett Cannon classifier / activity analysis
[057]
引用 [057]
这是趋势分析和预测,不是全生态已经完成的事实。
2016 package-release activity analysis and its forecast of Python 3 support overtaking Python 2
这个先后顺序很关键。一个维护者可以先发布支持 Python 3 的版本,因为他控制的是包的下一次 release;一个大学实验室、公司内部服务或旧 notebook 用户却可能被锁在 Python 2,因为他们还要等待上游依赖、系统镜像、插件或部署平台。包页面上的 classifier 先改变,不代表每一台正在运行的机器已经改变。
反过来,下载量也不能被当成“社区拒绝迁移”的证据。旧环境可能因为长期任务、镜像缓存或自动化构建反复请求旧版本;这些请求说明旧世界还在运行,却不能单独说明使用者对 Python 3 的态度。供给和消费的错位,正是十多年共存的可见结果。
[096]
引用 [096]
这里区分指标与用户处境;不能从下载量或 classifier 推出态度或统一采用率。
- Donald Stufft,`A Year of PyPI Downloads`;2015-04-20
- Brett Cannon,`Python 3 support on PyPI`;2015-06-12 / 13
- Christopher Wilcox,`Python 3 is Winning Library Developer Support`;2016-03-08
PyPI request logs, package classifiers and release activity: supply / consumption distinction
生态的变化先发生在维护者愿意发布什么,后来才发生在工具和发行版默认安装什么,最后才表现为旧项目发现自己再也拿不到同样的新版本。
[088]
引用 [088]
三种指标支撑供给与消费不能合并;不能推出统一采用率或单一拐点。
- Donald Stufft,`A Year of PyPI Downloads`;2015-04-20
- Brett Cannon,`Python 3 support on PyPI`;2015-06-12 / 13
- Christopher Wilcox,`Python 3 is Winning Library Developer Support`;2016-03-08
PyPI request analysis, package classifier / activity analysis, and package-release activity analysis
生态改变默认选项,不靠一枚开关完成,而是分层退出。
Python 3 Statement 给出共同的二零二零期限;Django 2.0 不再支持 Python 2;NumPy 的新 feature release 转为 Python 3 only,但保留旧兼容版本从 PyPI 安装。Fedora 32 从发行版中移除 python2,pip 21.0 又在工具层停止支持 Python 2。
所以“Python 2 结束”至少有几个不同的日期:上游 EOL、最后的 2.7.18、发行版移除、框架和科学计算包停止发新版本,以及 pip 不再为 Python 2 发布新支持。没有一个日期能替所有系统宣布迁移完成。
[058]
引用 [058]
三个项目材料支撑不同下游的公开退出动作;不能推出所有用户随项目日期同步迁移。
- Scientific Python 等项目,`Sunsetting Python 2 support`;约 2016 起
- Django 2.0 release notes;2017-12-02
- NumPy,NEP 14;resolution 2017-11
Python 3 Statement common deadline; Django 2.0 Python compatibility; NumPy NEP 14 Python 3-only feature releases
[059]
引用 [059]
发行版和工具链的退出时间各自独立;不能写成所有发行版同步,也不能说 Python 2 从此无法运行或安装任何包。
Fedora 32 Retire Python 2 change and pip 21.0 release notes
[060]
引用 [060]
固定上游 EOL 与最后版本两个时间点;不能把 2020 某一天写成所有生产系统完成迁移。
- Python.org,`Sunsetting Python 2`;2020
- Benjamin Peterson,`Python 2.7.18, the end of an era`;2020-04-20
Python.org sunset FAQ and Python 2.7.18 final release announcement
Django 2.0 的动作发生在框架层:新版本不再把 Python 2 当作支持对象。NumPy 的动作发生在 feature line:旧版本还可安装,但新功能沿 Python 3 前进。Fedora 的动作发生在操作系统仓库:如果 python2 及其依赖还要留下,就需要对整条依赖链承担例外。pip 21.0 的动作发生在工具层:最新的安装工具不再为 Python 2 继续提供支持。
这四扇门关闭的对象不同。一个项目可能已经能运行在 Python 3,却仍然被旧系统包卡住;也可能有 Python 3 解释器,却因为安装工具和依赖解析器停在旧版本。把这些日期压成一个“2020 年迁移完成”,就会删掉真正决定升级成本的那一层。
生态接受不是所有人同时走过终点线,而是每个公共入口逐渐停止替旧运行时提供下一站。留在过去仍然可能运行,区别只在于公共生态不再替它继续铺路。
[089]
引用 [089]
四份材料支撑不同生态层级的退出动作;不能说所有用户或发行版在同一天完成迁移。
- Django 2.0 release notes;2017-12-02
- NumPy,NEP 14;resolution 2017-11
- Fedora 32 `Retire Python 2` change
- pip 21.0 release notes;2021-01-23
Django 2.0, NumPy NEP 14, Fedora 32 Python 2 removal, and pip 21.0 Python 2 support removal
三种兼容预算
32:39Ruby 走的是“先把未来的断裂变成今天的 warning”。Ruby 2.7 对 positional arguments 和 keyword arguments 的自动转换发出弃用警告,并告诉开发者 Ruby 3 会移除这种转换。Ruby 3.0 再执行分离,同时带来 Ractor、类型工具等新的语言和运行时内容。
这不是 Python 3 的复刻,也不能凭一个 keyword argument 变化判断两次大版本规模相同。它能说明的是:一部分 breaking change 可以先被暴露,让调用者在旧版本仍能运行时修正。
Ruby 的关键不在 warning 这个词本身,而在 warning 和执行之间留下了一个仍能工作的版本。调用者可以在 Ruby 2.7 上看到旧的 positional / keyword argument 自动转换已经不再是未来的稳定假设,先修改调用方式,再在 Ruby 3.0 面对真正的分离。这个路径把“升级后才发现”变成“旧版本上就能看到”。
当然,warning 只有在项目能看见、能处理、能把它加入 CI 时才有用。一个被日志淹没的弃用提示,并不会自动降低迁移成本;它必须有明确的移除版本、可复现的触发位置和足够的重叠窗口。Ruby 资料能支撑这个发布顺序,不能支撑所有用户都完成了修正。
[061]
引用 [061]
Ruby 官方发布文档支撑预警与执行顺序;不能写成 Ruby 3 与 Python 3 同等规模,也不能写成 Ruby 直接借鉴 Python。
Ruby 2.7 release: keyword argument deprecation and Ruby 3 warning; Ruby 3.0 release: keyword separation, Ractor and removed $SAFE
所以 Ruby 给出的不是 Python 3 的答案,而是一种兼容预算:先让旧版本承认未来会改变,再把断裂交给新版本执行。
[090]
引用 [090]
Ruby 发布资料支撑 warning 到执行的窗口;不能推断全体用户迁移完成或与 Python 3 规模相同。
Ruby 2.7 keyword argument deprecation warnings and Ruby 3.0 keyword separation
Go 1 的选择更像在起点设置长期预算。
Go 官方承诺,符合 Go 1 规范的程序,在 Go 1 的后续 point release 中应继续编译和运行;这个承诺是源代码级的,不是二进制兼容。标准库可以增加 API,但不应以破坏现有 Go 1 代码的方式增长。文档同时保留安全问题、未规定行为、规范错误、编译器 bug 和少数结构体用法等例外。
“兼容”因此不是一句永远不变的口号,而是一份写出范围和例外的合同。Go 也保留未来出现 Go 2 规范的可能性,只是把日常 point release 的预期先固定下来。
这个合同把开发者最关心的风险提前限定了:今天符合 Go 1 规范的源代码,不能因为一次普通的 point release 就突然需要重写。它没有承诺二进制文件永远兼容,也没有承诺实现细节和未规定行为不变;它承诺的是一块被明确命名的源代码表面。
对比 Python 2 到 3,这种做法牺牲的是一部分日常设计自由,换来的是更容易安排的升级预期。未来如果真的要进入 Go 2,破坏性变化也必须面对一份已经被用户记住的合同;但在 Go 1 的日常发布里,大多数使用者不用每次点版本都重新计算整个生态的风险。
[062]
引用 [062]
Go 文档支撑源代码兼容承诺和例外;不能推出 Go 没有任何 breaking change,也不能说该策略由 Python 3 直接导致。
Go 1 compatibility document: source-level promise, standard API growth, point-release expectations and listed exceptions
Go 的经验不是“永远不要 break”,而是先把不 break 的范围写清楚,再把例外写清楚。兼容承诺越具体,维护者越知道自己在保护什么,使用者也越知道什么时候必须换一份合同。
[091]
引用 [091]
Go 文档支撑源代码合同及例外;不能说 Go 没有 breaking change。
Go 1 source-level compatibility promise, point-release scope, standard library growth, and listed exceptions
Python 后来的答案不是重新承诺永远兼容,而是把 feature release 变得更小、更规律。PEP 602 从 Python 3.9 起采用每年十月发布 feature version;它明确说,变化面变小、升级路径更渐进,同时保留弃用功能至少经过两个 release 才移除的政策。
这三种策略把成本放在不同地方:Ruby 先发警告,Go 把源码兼容写进主版本合同,Python 用更密集、更可预测的发布节奏减少一次积累的变化量。它们不是一张优劣榜,而是三种兼容预算。
对 Python 来说,这个变化更像是在承认上一轮断裂太大之后,发布节奏本身也是兼容机制。feature release 如果每年到来,单次带入的功能和弃用就有机会变小;弃用至少经过两个 release 才移除,则给维护者留下一个可以看到、修复和验证的窗口。
它并不保证升级永远顺滑。依赖的维护者仍要读 release notes,应用仍要跑测试,商业宿主仍可能有自己的日历。但与一次把多年清理集中到 Python 3 的大版本相比,年度节奏把“我要何时检查变化”变成了更规律的工作,而不是等到下一次大断裂才被提醒。
[063]
引用 [063]
PEP 602 与当前版本指南支撑流程策略和版本阶段;不能把它写成 Python 3 迁移的直接修复,也不能因此断言 Python 不再产生不兼容变化。
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
PEP 602 abstract, deprecations and rationale: annual feature releases, smaller changes, gradual upgrade path and at least two releases before removal; Developer's Guide version stages
这就是跨语言比较最后留下的共同问题:breaking change 不会凭空消失,团队只能决定把它放在 warning、合同、发布频率,还是一次集中的大版本里。
[092]
引用 [092]
Python 资料支撑发布节奏策略;不能写成 Python 3 迁移的直接修复或从此没有不兼容变化。
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
PEP 602 annual feature release cadence, gradual upgrade path, two-release deprecation window, and current version stages
回到现在与未来
37:20现在再回到那一行 print("你好")。
对一个初学者,它仍然可以是开始;对一个要在实验室、公司或特定领域临时解决问题的人,一个 .py 文件仍然可以很快带来结果。但语言的年龄意味着,代码不会只活在作者的电脑里。它会进入库、发行版、构建工具、桌面客户端和有自己生命周期的商业软件。
[097]
引用 [097]
结尾是基于已登记事实的综合判断;不把 Python 2 sunset 写成所有生产系统同日迁移,也不承诺未来没有 breaking change。
- Python.org,`Sunsetting Python 2`;2020
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
Python 2 sunset boundary, annual feature release policy, and current version lifecycle stages
当前 Developer's Guide 用 feature、bugfix、security 和 end-of-life 等阶段管理不同版本;PEP 602 的年度节奏则让功能更规律地抵达。它不是“Python 从此没有 breaking change”,而是把变化提前暴露、分散到更可预测的发布窗口里。
[098]
引用 [098]
当前结尾以 2026-05-27 快照为口径;页面会更新,不能写成 Python 不再有不兼容变化。
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
PEP 602 annual feature release and deprecation policy; Python Developer's Guide current feature / bugfix / security / end-of-life stages
这也改变了“现在的 Python”应该怎样被描述。对新项目,Python 3 是公共生态的默认路径;对一个还锁在 Python 2 的旧系统,现实问题可能仍然是插件、系统镜像、内部构建和没有替换方案的依赖。前者说明生态已经完成方向切换,后者说明方向切换不等于每个系统都已经支付完迁移成本。
[097]
引用 [097]
结尾是基于已登记事实的综合判断;不把 Python 2 sunset 写成所有生产系统同日迁移,也不承诺未来没有 breaking change。
- Python.org,`Sunsetting Python 2`;2020
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
Python 2 sunset boundary, annual feature release policy, and current version lifecycle stages
未来的 Python 仍会做不兼容的选择。差别在于,今天更成熟的做法应该让使用者尽早看到变化,让版本边界、弃用期限和例外条件可以被机器与人同时检查。一个 .py 文件要容易开始,一次升级也要有一个普通团队可以开始的第一步。
[097]
引用 [097]
结尾是基于已登记事实的综合判断;不把 Python 2 sunset 写成所有生产系统同日迁移,也不承诺未来没有 breaking change。
- Python.org,`Sunsetting Python 2`;2020
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
Python 2 sunset boundary, annual feature release policy, and current version lifecycle stages
如果把本期的故事只讲成 Python 2 输给了 Python 3,听众会错过最现实的部分:最后赢下来的不是某一套语法,而是一组让生态逐步停止携带过去的安排。
[093]
引用 [093]
结尾区分公共默认路径与遗留系统;不能把 Python 2 sunset 写成所有生产系统同日迁移。
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
- Python.org,`Sunsetting Python 2`;2020
Python annual feature cadence, current version stages, and official Python 2 sunset boundary
Python 2 到 Python 3 没有一个瞬间把所有过去搬走。它更像几层门依次关闭:核心团队停止发展旧语义,兼容工具把断裂拆成工作步骤,包维护者划出旧版边界,企业用双解释器和灰度保护用户,发行版与 pip 最后撤走默认兜底。
[097]
引用 [097]
结尾是基于已登记事实的综合判断;不把 Python 2 sunset 写成所有生产系统同日迁移,也不承诺未来没有 breaking change。
- Python.org,`Sunsetting Python 2`;2020
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
Python 2 sunset boundary, annual feature release policy, and current version lifecycle stages
所以本期的结论不是“核心团队当年做对了,社区后来终于想通了”。更准确的说法是:语言设计者把长期收益和近期成本分给了不同的人;社区花了十多年发明中间状态;生态最终通过共同期限和停止支持,把 Python 3 变成多数活跃项目的默认路径,而不是所有遗留系统的同时终点。
[097]
引用 [097]
结尾是基于已登记事实的综合判断;不把 Python 2 sunset 写成所有生产系统同日迁移,也不承诺未来没有 breaking change。
- Python.org,`Sunsetting Python 2`;2020
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
Python 2 sunset boundary, annual feature release policy, and current version lifecycle stages
Python 仍然要面对未来的 breaking change。真正值得继承的经验,也许不是永远不变,而是让每一次变化都有可见的 warning、可复查的边界、足够的中间版本,以及一条普通使用者能够开始的迁移路径。
[097]
引用 [097]
结尾是基于已登记事实的综合判断;不把 Python 2 sunset 写成所有生产系统同日迁移,也不承诺未来没有 breaking change。
- Python.org,`Sunsetting Python 2`;2020
- Łukasz Langa,PEP 602;2019-09-06
- Python Developer's Guide,`Status of Python versions`;当前快照修改于 2026-05-27
Python 2 sunset boundary, annual feature release policy, and current version lifecycle stages
片尾口播
40:07你刚刚收听的是《原代码》第十期,《回到未来》。
[099]
引用 [099]
本段只负责节目识别,不把片尾文案当作 Python 2 历史事实主张。
Episode identity and title; source entry retained only for the episode's historical subject context
你可以访问《原代码》的官方网站。网站地址是,原代码三个字的全拼,点 X Y Z。在那里,你可以查看本期逐字稿、证据引用关系和证据快照,也可以收听往期节目。
感谢你的收听。
我们下期再见。
[100]
引用 [100]
本段是固定官网入口与告别,不承诺当前尚未通过公开门禁的材料已经发布。
Standard Chinese outro website module and episode publication-materials boundary