本节目标:理解 PEP 从提案到落地/弃用的生命周期,掌握
__future__、warnings与sys.version_info三件套,并逐条确认 3.14 的真实迁移点。
适用版本:Python 3.12+(实测 3.14.6)
11.3 PEP 流程与版本迁移策略
前面十节讲的字节码、内存、自由线程、打包,背后都是一个个 PEP。本节收尾:这些变化是怎么被提出、被接受、被弃用,最终落到你的项目里的。
11.3.1 PEP 的生命周期
PEP 是 Python 增强提案(Python Enhancement Proposal)。PEP 1 规定格式,PEP 12 规定模板;按类型分三类:
| 类型 | 含义 | 例 |
|---|---|---|
| Standards Track | 改语言或标准库 | PEP 649(注解延迟求值) |
| Informational | 提供信息,不强制 | PEP 8(风格指南) |
| Process | 改流程本身 | PEP 1、PEP 13(治理) |
状态机是 Draft → (Provisional) → Accepted → Final;未采纳的走向 Rejected / Withdrawn / Deferred,被新提案取代则 Superseded。Provisional 这个词最关键:它表示「已合入 CPython 但 API 仍可能变」——3.13 的自由线程(PEP 703)当时就是 Provisional,所以那会儿不该依赖它。一个提案走完要多久?PEP 649 从 2020 年提出到 2025 年在 3.14 落地,跨了约 5 年——别指望 PEP 一被接受就能立刻用上。
11.3.2 __future__:语言级灰度开关
__future__ 是 CPython 提供的「提前启用未来特性」机制:某语法先挂在 __future__ 下可用,等成为默认行为后再移除该开关。
import __future__
print(__future__.all_feature_names)
['nested_scopes', 'generators', 'division', 'absolute_import', 'with_statement',
'print_function', 'unicode_literals', 'barry_as_FLUFL', 'generator_stop', 'annotations']
这张表本身就是一部语言演化史:print_function(2→3 的 print 函数化)、unicode_literals、division 都是为 Python 3 过渡铺路的开关,如今早已默认生效。注意 annotations(PEP 563)在 3.14 仍留在列表中——尽管 PEP 649 已把「注解延迟求值」变成默认行为,from __future__ import annotations 语句依然合法(保留向后兼容),只是它的语义与 PEP 649 的默认行为并不完全相同。
11.3.3 弃用周期:从警告到移除
标准库的 API 不会突然消失,而是走一条三级弃用阶梯:
| 阶段 | 警告类别 | 默认是否可见 | 含义 |
|---|---|---|---|
| 计划弃用 | PendingDeprecationWarning | 否 | 已计划移除,但还没到「别用」 |
| 弃用 | DeprecationWarning | 否(__main__ 内可见) | 别再用,将来会移除 |
| 移除 | 抛异常或删除 | — | 已下线 |
实测一个真实的弃用 API(datetime.utcnow(),3.12 起弃用):
import warnings, datetime
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
datetime.datetime.utcnow()
print([f"{x.category.__name__}: {x.message}" for x in w])
['DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for
removal in a future version. Use timezone-aware objects to represent datetimes in
UTC: datetime.datetime.now(datetime.UTC).']
3.13 起,库作者可以更省事地标记弃用——warnings.deprecated 装饰器:
from warnings import deprecated
@deprecated("use new_fn instead")
def old_fn():
return 1
import warnings
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
old_fn()
print([f"{x.category.__name__}: {x.message}" for x in w])
['DeprecationWarning: use new_fn instead']
要留意默认过滤器:DeprecationWarning 默认只对 __main__ 可见,所以直接跑上面的 utcnow() 会打印,但若它是由第三方库内部触发的,默认是静默的。这是 CPython 的刻意设计——避免用户被依赖里的弃用噪音淹没。
11.3.4 特性检测:运行时与元数据两条线
迁移第一步是知道自己在什么版本上跑。运行时用 sys.version_info:
import sys
print(sys.version_info[:3]) # (3, 14, 6)
print(sys.version_info >= (3, 12)) # True
print(sys.version_info >= (3, 13, 0, "final")) # True
print(sys.version_info >= (3, 15)) # False
(3, 14, 6)
True
True
False
sys.version_info 是元组,比较时从左到右、遇到不同即定论,所以 (3, 14, 6) >= (3, 12) 为真、>= (3, 15) 为假。它比 sys.version 字符串可靠得多——字符串比较会得出 "3.9" > "3.14" 这种错误结论。
另一条线是元数据声明——requires-python,它约束的是目标解释器版本:
[project]
requires-python = ">=3.12"
CI 里把它落成矩阵,一处声明、处处验证:
# .github/workflows/ci.yml(节选)
jobs:
test:
strategy:
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -e .
- run: pytest
(该工作流为示意,本机未执行 GitHub Actions。)矩阵的三个版本正好对应本卷的特性分界:3.12 是基线、3.13 引入自由线程与 JIT、3.14 是当前稳定线。
11.3.5 3.14 的具体迁移点(逐条实测)
下面每一条都在本机 3.14.6 上跑过。
① PEP 649/749:注解不再在定义时求值。 这是 3.14 最容易踩的坑:
src = "def f(x: Undefined_Name) -> AlsoMissing:\n return x\n"
ns = {}
exec(src, ns) # 定义成功——注解不求值
print(ns["f"].__annotations__) # 访问时才求值
exec 定义成功,未触发 NameError
访问 __annotations__ -> NameError: name 'Undefined_Name' is not defined
注解被延迟到首次访问 __annotations__ 时才求值,且函数对象上多了 __annotate__。要按不同格式取值,用新模块 annotationlib,它提供 Format.VALUE(正常求值)、Format.FORWARDREF(未定义名转成 ForwardRef 而不报错)、Format.STRING(源码字符串)、Format.VALUE_WITH_FAKE_GLOBALS 四种。凡是用 get_type_hints 或直接读 __annotations__ 的代码,迁移时都要重新验证——第 8.3 注解与 PEP 649
有完整展开。
② PEP 765:finally 里的控制流降级为警告。
import warnings
bad = "def g():\n try:\n return 1\n finally:\n return 2\n"
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
compile(bad, "<s>", "exec")
print([f"{x.category.__name__}: {x.message}" for x in w])
["SyntaxWarning: 'return' in a 'finally' block"]
return / break / continue 出现在 finally 里会吞掉异常,是经典 bug 源。3.14 把它从「静默允许」降级为 SyntaxWarning(注意:是警告不是错误,代码仍能编译运行)。
③ PEP 784:标准库新增 compression.zstd。
from compression import zstd
data = b"hello zstd " * 100
c = zstd.compress(data)
print(len(data), len(c), zstd.decompress(c) == data)
1100 28 True
模块名是 compression.zstd,不是顶层 zstd——后者是第三方库的名字,两者不要混。1100 字节压到 28 字节,往返一致。
④ 多进程默认启动方式改了。 3.14 把默认启动方式换成「线程安全友好」的一种:
import multiprocessing as mp
print(mp.get_start_method()) # 本机 macOS: spawn
print(mp.get_all_start_methods()) # ['spawn', 'fork', 'forkserver']
default: spawn
all : ['spawn', 'fork', 'forkserver']
本机 macOS 显示 spawn,但这不是 3.14 新引入的(macOS 自 3.8 起就是 spawn)。真正的变化在 Linux,标准库源码 multiprocessing/context.py 里写着:
# gh-84559: We changed everyones default to a thread safeish one in 3.14.
if reduction.HAVE_SEND_HANDLE and sys.platform != 'darwin':
_default_context = DefaultContext(_concrete_contexts['forkserver'])
else:
_default_context = DefaultContext(_concrete_contexts['spawn'])
即 Linux 默认从 fork 改成 forkserver(macOS / Windows 仍是 spawn)。影响:fork 下依赖「父进程已导入的模块与全局状态被继承」的代码,在 forkserver 下可能拿不到——显式传参而不是靠继承才是安全写法。本机是 macOS,无法直接验证 Linux 行为,此条依据标准库源码与 gh-84559。
⑤ PEP 758:except 可省括号(但有条件)。
# 无 as 子句:可省括号
try:
raise ValueError("x")
except ValueError, TypeError:
print("caught without parens")
caught without parens
而一旦要绑定异常对象,括号仍不能省——except ValueError, TypeError as e: 会直接报 SyntaxError: multiple exception types must be parenthesized when using 'as'。省括号只在没有 as 子句时成立。
| 迁移点 | 3.14 行为 | 是否破坏兼容 |
|---|---|---|
| PEP 649 注解延迟求值 | 定义时不求值,访问时求值 | 是(依赖 __annotations__ 的代码需复测) |
PEP 765 finally 控制流 | SyntaxWarning(非错误) | 否(但应尽快修) |
PEP 784 compression.zstd | 新增标准库模块 | 否(纯增量) |
| 多进程默认启动方式 | Linux fork → forkserver | 是(依赖 fork 继承的代码) |
PEP 758 except 免括号 | 无 as 时可省括号 | 否(纯增量) |
11.3.6 迁移策略:把警告变成门禁
把上面这些串成一套可执行的迁移流程:
- 声明边界:
requires-python写清支持区间,CI 矩阵覆盖上下界。 - 打开警告:CI 里加
-W error::DeprecationWarning(或PYTHONWARNINGS=error::DeprecationWarning),让弃用当场变成失败,而不是淹没在日志里。 - 先验证再抬门槛:升级大版本前,先在矩阵里加一列新版本、跑一遍测试,确认通过后再改
requires-python下限。 - 自动化改写:
ruff的UP规则集、pyupgrade能自动处理一部分语法升级;但语义类变化(如 PEP 649)无法自动改,只能靠测试兜底。 - 给弃用留预算:每个
DeprecationWarning记一条 issue,在它被移除的那个版本发布前修完。
小结
- PEP 分 Standards Track / Informational / Process 三类,生命周期是
Draft → Provisional → Accepted → Final;Provisional 意味着 API 还可能变,别急着依赖。 __future__是语言级灰度开关;from __future__ import annotations在 3.14 仍合法,但语义与 PEP 649 的默认行为不完全相同。- 弃用走
PendingDeprecationWarning → DeprecationWarning → 移除三级阶梯;实测datetime.utcnow()与warnings.deprecated(3.13)都发出DeprecationWarning。 - 特性检测两条线:运行时用
sys.version_info(元组比较可靠),元数据用requires-python+ CI 矩阵。 - 3.14 五个迁移点实测:PEP 649 注解延迟求值(破坏性)、PEP 765
finally控制流降为SyntaxWarning、PEP 784compression.zstd、多进程 Linux 默认改forkserver、PEP 758except无as时可省括号。 - 迁移策略的核心是把警告变成门禁:CI 里
-W error::DeprecationWarning,让弃用当场失败,再用矩阵验证新版本行为。
到这里,《Python高级编程》的正文全部讲完。回到 全书目录 ,可以按章节顺序重读,或挑感兴趣的专题深入。
阅读导航:上一节:11.2 嵌入式与自由线程运行时 · 下一节:全书目录 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。