Python 3.15.0b1 在上个月发布了。我没急着写——beta 版本嘛,谁知道后面还会不会改。但现在 beta 2 出来,feature freeze 已经锁死,该聊了。
先给你看个东西:
# 以前:import 就加载,哪怕这个函数根本不会走到
import heavy_json_processor # 启动 +300ms
def handle_request(data):
if data["type"] == "simple":
return data # json 模块白加载了
return heavy_json_processor.process(data)
# Python 3.15:用的时候才加载
lazy import heavy_json_processor
def handle_request(data):
if data["type"] == "simple":
return data # 根本没加载,省了 300ms
return heavy_json_processor.process(data)
就这一行 lazy,你的 CLI 工具启动可能快一半。先记着这个,后面展开。
TL;DR
- Python 3.15 不是功能大爆炸版本——它是把散布在第三方的最佳实践(懒加载、不可变映射、采样 profiler)吸收为语言基础设施的"工程化补齐"版本
- 6 个跟数据工程师直接相关的变化:lazy import / frozendict / 推导式解包 / JIT 升级 / Tachyon profiler / GC 回退
- 不改一行代码,ARM Mac 上的 Python 跑快 12-13%。改了的话——lazy import 能让 CLI 启动减半
- 升级姿势:用
pyenv install 3.15.0b2 本地测;生产等到 10 月正式版
01版本疲劳?这次不一样
我知道你在想什么。
Python 3.13 说 JIT 来了,3.14 说 GC 重写了,3.15——又一个版本号。大部分 Python 开发者还在 3.11 上跑着,懒得动。
但 3.15 有个微妙的区别:它不要求你学新概念。
lazy import 就是在 import 前面加一个词。frozendict 就是你一直在用 tuple(sorted(d.items())) 做的那个事,现在原生支持了。JIT 升级是你睡一觉起来它就快了的改动。
而且 3.14 那个出问题的增量 GC——在 3.15 修好了。直接回退到 3.13 的分代回收,稳了。所以如果你跳过了 3.14,3.15 是更好的起跳点。
真正让我觉得这版本值得写一篇文章的,不是某个单一特性,而是看完 14 个 PEP 之后的一个判断——Python 正在完成它的"工程化成人礼"。
一个一个说。
02三层结构:你看得见的、看不见的、和地基
如果把 Python 3.15 的改动摊开来看,它们自然分成三层:
┌─────────────────────────────┐
│ Syntax Surface(语法层) │ ← 你每天敲的代码
│ lazy import / frozendict │
│ unpack comprehension │
├─────────────────────────────┤
│ Performance Engine(性能层) │ ← 你看不见但感受得到
│ JIT 升级 / Tachyon profiler │
│ frame pointers │
├─────────────────────────────┤
│ Ecosystem Foundation(地基) │ ← 平时不关心,塌了是灾难
│ UTF-8 默认 / GC 回退 │
│ .start 文件 / abi3t │
└─────────────────────────────┘
这个分层不是官方分类——是我看完 14 个 PEP 之后自己理出来的。每一层解决一个层级的问题,合在一起回答一个问题:一个 34 岁的语言,怎么在不破坏生态的前提下继续变快、变安全、变更可观测?
语法层解决"写得爽不爽"。性能层解决"跑得快不快"。地基解决"部署时会不会炸"。
下面按层拆开。每层挑跟你最相关的说。
03语法层:三个新工具,替换你的 workaround
lazy import:让 import 变懒
Python 的 import 是 eager 的——import 语句执行时立刻加载整个模块。这在 CLI 工具和多服务应用中特别伤:你可能 import 了 20 个模块,但实际处理一个请求只需要其中 2 个。
3.15 的解法是一个新的软关键字 lazy:
lazy import json
lazy from pathlib import Path
lazy import numpy as np
lazy from .utils import heavy_parser
# 以上四行不加载任何模块。加载发生在第一次使用时
data = json.loads('{"a": 1}') # json 在这一行才加载
如果加载失败(比如模块不存在),报错也推迟到使用点,traceback 里会同时显示调用位置和原始 import 语句。
全局控制有三种方式:
# 全部懒加载(激进——可能破坏依赖 import 副作用的代码)
export PYTHON_LAZY_IMPORTS=all
# 命令行启动
python -X lazy_imports=all your_script.py
更安全的做法是用 filter 函数,只懒加载你自己的模块:
import sys
def my_filter(importing, imported, fromlist):
return imported.startswith("myapp.")
sys.set_lazy_imports_filter(my_filter)
sys.set_lazy_imports("all")
import myapp.slow_module # 懒加载
import json # 立即加载(不受影响)
兼容旧版本 Python 的项目可以用 __lazy_modules__ 列表做向后兼容——3.15 以下版本直接忽略这个变量,行为不变。
跟你有什么关系:如果你写 CLI 工具、API 服务、或者 import 了 torch/tensorflow 这种重型库但只用其中少数函数——lazy import 是 3.15 最直接可用的性能提升。
frozendict:终于不用 tuple(sorted(d.items())) 了
Python 的 dict 一直有个尴尬:你需要一个不可变的、可哈希的映射来做缓存 key 或配置常量,但 dict 本身可变且不可哈希。
以前的 workaround:
# 又丑又慢
CACHE_KEY = tuple(sorted(config.items()))
# 或者用 MappingProxyType,但它不是 dict-like
from types import MappingProxyType
CONFIG = MappingProxyType({"host": "localhost", "port": 5432})
3.15 直接给了 frozendict——一个新的 builtins 类型:
>>> config = frozendict(host="localhost", port=5432)
>>> config["host"]
'localhost'
>>> config["port"] = 3306
TypeError: 'frozendict' object does not support item assignment
# 可哈希,直接做 dict key 或 set 元素
>>> cache = {}
>>> cache[frozendict(a=1, b=2)] = "result"
# 比较跟插入顺序无关
>>> a = frozendict(x=1, y=2)
>>> b = frozendict(y=2, x=1)
>>> a == b # True
>>> hash(a) == hash(b) # True
json、pickle、pprint、copy、marshal 这些标准库模块已经适配了 frozendict。
一个需要注意的迁移点:如果你代码里用了 isinstance(x, dict) 做类型检查,frozendict 不会被匹配到(它不继承 dict)。改成 isinstance(x, collections.abc.Mapping) 就能同时覆盖 dict 和 frozendict。
推导式里终于能 unpack 了
这个说起来简单,但确实好用:
# 以前:嵌套推导,可读性一塌糊涂
>>> lists = [[1, 2], [3, 4], [5]]
>>> [x for L in lists for x in L]
[1, 2, 3, 4, 5]
# 3.15:直接 unpack
>>> [*L for L in lists]
[1, 2, 3, 4, 5]
# dict 也支持 **
>>> dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
>>> {**d for d in dicts}
{'a': 3, 'b': 2}
小语法糖,但写数据处理 pipeline 的时候少嵌套一层就是一层。
04性能层:不改代码,它就变快了
JIT:ARM Mac 上快 12-13%
Python 3.13 引入了 JIT 编译器,3.14 继续优化,3.15 上了"显著升级"(官方原话)。具体数字:
- Apple Silicon (ARM64) macOS:几何平均提升 12-13%
- Windows 64-bit:官方二进制现在默认启用 tail-calling interpreter
这些提升不需要你改任何代码。升级 Python 版本就有了。而且 12-13% 在 ARM Mac 上不是小数点后两位——是完全可以感知的差距。如果你的数据处理脚本原来跑 10 秒,现在 8.7 秒。
Tachyon:Python 终于有个像样的 profiler 了
这是 3.15 我最期待的特性。
以前的 Python profiling 是两难:cProfile 是 deterministic tracing,本身有大量开销,测出来的结果跟真实性能偏差不小。timeit 只能测微基准。py-spy 是第三方工具,需要单独安装,功能有限。
3.15 的 Tachyon 是内置的
统计采样 profiler——原理类似 Linux perf,高频采样调用栈,几乎零开销:
# 直接运行并采样
python -m profiling.sampling your_script.py
# 附加到正在运行的进程(不需要重启!)
python -m profiling.sampling --attach 12345
# 四种采样模式
python -m profiling.sampling --mode wall your_script.py # 墙钟时间(含 I/O 等待)
python -m profiling.sampling --mode cpu your_script.py # 纯 CPU 时间
python -m profiling.sampling --mode gil your_script.py # GIL 持有时间
python -m profiling.sampling --mode exception your_script.py # 异常线程采样
输出格式也覆盖了主流生态:
# 生成可交互的 HTML flame graph
python -m profiling.sampling --flamegraph output.html your_script.py
# pstats 兼容格式
python -m profiling.sampling --pstats output.prof your_script.py
# Firefox Profiler 格式
python -m profiling.sampling --gecko output.json your_script.py
# 源码行级 heatmap
python -m profiling.sampling --heatmap output.html your_script.py
# 实时 TUI(top-like 界面)
python -m profiling.sampling --live your_script.py
对 async 代码有 --async-aware 标志,会把异步任务的调用栈正确重建出来——以前的 profiler 面对 asyncio 代码基本抓瞎。
跟你有什么关系:如果你的 Python 服务有性能问题,以前你可能靠 print(time.time()) 排查。现在一行命令 attach 到生产进程,30 秒后拿到 flame graph。3.15 之前这是做不到的。
Frame pointers 默认开启
这个改动对普通用户不可见,但很重要。
CPython 3.15 在编译时默认开启了 frame pointers(-fno-omit-frame-pointer)。这意味着系统级的 profiler(perf、eBPF 工具、调试器)可以正确展开 Python 的调用栈——以前混合 C 扩展和 Python 代码的栈追踪经常断裂或不完整。
对于用 eBPF 做性能可观测性的团队,这是一个很大的改进。
05⚠️ L3 进阶区:生态地基的三个底层变化
以下内容偏底层。如果你只关心"能不能升级、怎么升级",跳到下一节就行。
UTF-8 成为默认编码
Python 3.15 开始,open('file.txt') 默认用 UTF-8 编码,不再依赖操作系统的 locale 设置。这意味着:
- macOS 上
open('file.txt').read() 不会因为 locale 是 C 而在非 ASCII 字符上报错 - Linux 服务器上用 Docker 跑 Python,不会再出现
UnicodeDecodeError 因为容器里 locale 没配
需要恢复旧行为的,两个途径:
export PYTHONUTF8=0
# 或
python -X utf8=0 your_script.py
单个文件级:open('file.txt', encoding='locale')。
GC 回退:3.14 的增量 GC 被撤回了
Python 3.14 引入了一个新的增量垃圾回收器,目标是降低 GC 暂停时间。但多个生产环境用户报告了"显著的内存压力"(CPython issue gh-142516)。于是核心团队做了一个成熟的决定:在 3.14.5 和 3.15 中回退到 3.13 的分代 GC。
这不是失败——这是一个 34 岁语言的核心团队在说"稳定性比新特性重要"。增量 GC 可能经过正式的 PEP 流程后在 3.16 回归。
对你的影响:如果你在 3.14.0-3.14.4 上跑生产,立刻升到 3.14.5+ 或直接跳到 3.15。如果你在 3.13 以下,不用管这事。
.start 文件替代 .pth 的 import hack
.pth 文件可以往 sys.path 加路径,但有个历史遗留功能——import 行会在 Python 启动时执行。这个功能一直被滥用,安全风险很大。
3.15 引入了 .start 文件,用明确的入口点格式 pkg.mod:callable 替代。同时 .pth 中的 import 行被静默废弃。
如果你的项目用了 .pth 文件做启动时初始化——该迁移了。
06数据工程师的 3.15 迁移指南
别急着
pip install --upgrade。按这个步骤来:
第一步:本地安装测试版
pyenv install 3.15.0b2
pyenv local 3.15.0b2
pip install -r requirements.txt # 看有没有装不上的包
pytest # 跑一遍测试
哪些项目可以现在就开始测:
- 纯 Python 的 CLI 工具和脚本——直接跑,大概率没问题
- 数据处理 pipeline(Pandas/Polars)——JIT 加速白捡的
- asyncio 应用——Tachyon profiler 的 async-aware 模式值得一试
哪些项目等到 10 月正式版:
- 生产环境 API 服务(第一个 beta 不建议)
- 依赖特定 3.14 GC 行为的项目(3.15 回退了,行为不同)
第二步:CI 配置
# GitHub Actions
jobs:
test:
strategy:
matrix:
python-version: ["3.13", "3.15.0-beta.2"]
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
先在 CI 矩阵里加上 3.15 做 canary——允许失败,但能提前发现问题。
第三步:lazy import 渐进式迁移
提升启动速度最容易的切入点:
# 在 __init__.py 或入口模块顶部
__lazy_modules__ = [
"heavy_parser",
"report_generator",
"email_sender",
]
这行代码在 3.15 以下版本被忽略,在 3.15+ 自动生效——零风险,渐进式。找一个 import 重但使用少的模块开始试。
07Python 的工程化成人礼
回顾 Python 最近三年的版本轨迹:
- 3.12(2023)→ 类型系统大年:泛型语法简化(PEP 695)、
override 装饰器 - 3.13(2024)→ 性能元年:JIT 编译器首次引入、GIL 变为可选
- 3.14(2025)→ GC 尝试重写……然后撤回
- 3.15(2026)→ 静默补齐:懒加载、不可变映射、profiler 原生化、编码统一
趋势很清晰:Python 不再需要通过"大功能"证明自己的生命力。它在做一件更难的事——把散布在生态里被验证了十年、二十年的最佳实践,一个一个收进语言核心。
frozendict 收了 types.MappingProxyType 和无数个"用 tuple(sorted(d.items())) 假装不可变 dict"的 workaround。lazy import 收了 importlib.util.LazyLoader 和各框架自己造的懒加载轮子。Tachyon 收了 py-spy、austin、scalene 这些第三方 profiler 的需求。
这个"回收"过程不如新语法热闹,但它回答了一个更根本的问题:怎么让 Python 在下一个十年仍然是"对的选择"。答案不是变成 Rust,也不是变成 Go,而是——变得更快、更可观测、更可预测。
3.14 那波增量 GC 我差点在线上开了——还好看到 GitHub issue 的 memory pressure 报告后踩了刹车。这是我第一次因为"观望"而庆幸的版本升级。
收进标准库是一回事,第三方会不会跟进是另一回事。frozendict 很好,但 Django ORM 和 SQLAlchemy 什么时候不再用 isinstance(x, dict) 做类型检查?生态适配才是真正的瓶颈。
3.15 完成这一步的方式很 Python:不激进,不破坏,把正确的东西默默做好。
我打算拿一个内部的 CLI 工具开刀——它 import 了 20 个模块但每个子命令只用 2 个。__lazy_modules__ 加一行,不改逻辑,启动时间能减多少我自己也很好奇。
08升级检查清单
把下面这个存成 python315_checklist.md,升级时对照:
## Python 3.15 升级检查清单
### 安装
- [ ] pyenv install 3.15.0b2
- [ ] pip install -r requirements.txt(记录装不上的包)
- [ ] pytest / 项目测试套件(记录失败的 case)
### 代码适配
- [ ] isinstance(x, dict) → isinstance(x, (dict, frozendict)) 或 collections.abc.Mapping
- [ ] open() 不带 encoding 的调用——行为从 locale 编码变为 UTF-8,确认无影响
- [ ] .pth 文件中的 import 行 → 迁移到 .start 文件
- [ ] 如有自定义 lazy import filter,配置 sys.set_lazy_imports_filter()
### 性能验证
- [ ] 用 Tachyon 跑一次采样:python -m profiling.sampling --flamegraph perf.html main.py
- [ ] 对比 3.14 vs 3.15 的端到端执行时间
- [ ] 如有 asyncio 代码,加 --async-aware 标志验证任务栈
### 生产部署
- [ ] 等待 3.15.0 正式版(2026-10-01)
- [ ] CI 矩阵加 3.15
- [ ] 灰度发布:先 10% 流量