Python 3.15 的 lazy import,启动慢是不是先把重模块标成 lazy 就行?

光跑 --help 也要等一截——命令行工具加了几个可选子命令之后,导入树太深,很多模块这一趟根本用不上,却在启动时全加载了。以前常见权宜之计是把 import 塞进函数里,可读性和静态分析都别扭。Python 3.15 引入了显式的 lazy import(PEP 810),可以把「声明放在文件顶」和「真正加载拖到第一次用」拆开。python.org 下载列表里,3.15 系列当前补丁是 3.15.2(3.15.0 发布于 2026 年 10 月 9 日,之后有 3.15.1 / 3.15.2)。下面说的行为以 What’s New in Python 3.15 和 PEP 810 为准,不谈默认 UTF-8 那条已经单独写过的改动。

语法和推迟点

可以直接写:

lazy import json
lazy from pathlib import Path

名字立刻绑到当前模块,真正加载拖到第一次用这个名字。加载失败时,异常出现在首次使用处,而不是 import 那一行。回溯里会带上原先的 lazy import 位置,方便对。lazy from module import a, b 时,第一次用到其中一个名字会加载整个模块,并只把那一个名字实体化,另一个仍保持代理,直到被访问。代理类型是 types.LazyImportType,需要程序化探测时可以用它。

只允许在模块顶层。函数里、类体里、try / except / finally 里写 lazy,或 lazy from … import *、lazy from __future__ import …,都会 SyntaxError。也就是说,想在函数里临时懒加载,还是得用传统的函数内 import,或者把声明挪回模块顶并加 lazy。

全局旗标和 filter

不想改源码、又想整进程偏懒,可以用 -X lazy_imports=all 或环境变量 PYTHON_LAZY_IMPORTS=all。normal(默认)只尊重源码里的 lazy 关键字。none 则强迫全部急切加载,方便排查。运行时还有 sys.set_lazy_imports() / sys.get_lazy_imports()。官方把全局开关和 filter 定位成偏应用 / 运维侧的高级能力,库作者一般不该在 import 时偷偷改全局模式。

更细的是 sys.set_lazy_imports_filter():回调收到导入方、被导入模块名、fromlist,返回 True 才允许继续懒加载。常见思路是「自家包前缀走 lazy,第三方保持 eager」,避免有些库依赖 import 时副作用(注册插件、改全局状态)被推迟搞乱:

import sys

def myapp_filter(importing, imported, fromlist):
    return imported.startswith("myapp.")

sys.set_lazy_imports_filter(myapp_filter)
sys.set_lazy_imports("all")

还要兼容旧版本时,可以在模块里定义 __lazy_modules__ 列表。3.15+ 会把列表里的普通 import 当成 lazy,老解释器会忽略这个名字。官方把它定位成过渡手段,新代码优先直接写 lazy 关键字。

别指望的东西

  • 不承诺「启动一定少几秒」。收益取决于有多少导入其实用不到,以及副作用能不能安全延后。CLI 多子命令、可选重依赖这类场景通常更有感,但具体数字因项目而异。
  • all 一开,带注册副作用的第三方库可能表现怪异。出问题就用 filter 拉回 eager,或临时 lazy_imports=none 排查。
  • 注解专用的 import 很适合标 lazy,常能少写一层 TYPE_CHECKING 护栏,但不等于可以省掉对副作用模块的审查。
  • from module import * 不能 lazy。全局 all 模式下星号导入仍是急切的。
  • 懒加载只推迟「何时 import」,不改变「一次会执行整个模块」。lazy from heavy import OneClass 仍然会在首次使用时跑完整个 heavy。

落地顺序可以很保守:先给真正拖慢启动、又确定无 import 副作用的可选依赖加 lazy。再用 -X importtime 一类手段看还剩哪些大户。最后才考虑进程级 all 加 filter。类型检查器一般仍把 lazy import 当普通名字解析,注解场景往往最省事。热路径上第一次访问会付一次真实 import 成本,之后自适应解释器会把慢路径特化掉,所以别把「懒」理解成每次访问都要重新判断。

你们升 3.15 以后,是逐个加 lazy,还是直接 -X lazy_imports=all 再 filter?有没有库一懒加载就注册丢了的?