0%

Python 3.15 惰性导入:让 import 推迟到第一次使用

【由DeepSeek辅助编写】

Python 3.15 在 2026 年 10 月 9 日发布(release notes)。PEP 810 带来的显式惰性导入大概是这一版里性价比最高的改动:在 import 前面加一个 lazy 关键字,模块就不在语句执行时加载,而是等你第一次真正读到这个名字才加载。对启动时间敏感的 CLI 工具、serverless 冷启动这类场景,这可能是一行关键字换来的一次优化。

先验证:import 真的被推迟了吗

mod.py 在被导入时会打印一行:

1
2
3
4
print('mod loaded')

def hello():
print("hello from mod")

main.py 只把 import 换成 lazy import:

1
2
3
4
5
6
7
8
# uv run -p 3.15 python -m main
lazy import mod

print('before calling hello')

mod.hello()

print('after calling hello')

在 CPython 3.15.0(Windows x64)上运行 python -m main,输出是:

1
2
3
4
before calling hello
mod loaded
hello from mod
after calling hello

第二行是关键:mod loaded 出现在 before calling hello 之后。如果写的是普通 import mod,它会在第一行就打印。再反过来验证一次——只写 lazy import mod、全程不碰 mod,那么 mod loaded 一次都不会出现,模块根本没有进入内存。

uv 用户注意:注释里那条 uv run -p 3.15 python -m main 是合法写法(-p 就是 --python)。不过截至 uv 0.12.17,3.15 会解析到 3.15.0rc2;想跑正式版,直接 python3.15 -m main 更稳。

三种写法

1
2
lazy import mod                # 整个模块推迟
lazy from mod import hello # 模块和每个名字都推迟

lazy from ... import ... 的语义稍微特别一点:第一次访问其中任何一个名字,就会加载整个模块,但只有被访问的那个名字会被解析成真实值,其余名字仍然是代理,直到你访问它们。

不想改 import 语句的话,可以在模块里声明 __lazy_modules__:

1
2
3
4
5
__lazy_modules__ = ["json", "pathlib"]   # 必须写全限定名

import json # 惰性
import os # 照旧立即加载
import pathlib # 惰性

还可以在进程级别开关:python -X lazy_imports=all(或环境变量 PYTHON_LAZY_IMPORTS=all)会把所有普通 import 都变成惰性的;默认值是 normal,即只有显式写了 lazy 才惰性。运行时可以用 sys.set_lazy_imports() 调整。

绑定的是代理,不是模块

语句执行后,名字上绑的是一个惰性代理对象:

1
2
3
4
5
lazy import mod

'mod' in sys.modules # False
repr(globals()['mod']) # "<lazy_import 'mod'>"
isinstance(globals()['mod'], types.LazyImportType) # True

需要提前加载时,先取出代理再显式解析:globals()['mod'].resolve()。

这里有个容易踩的点:解析发生在“读取这个名字”的时候。mod.hello() 会加载,但 type(mod)、dir(mod)、甚至 x = mod 同样会加载——只要字节码读了全局名字就会触发。真正不触发的是 globals()、无参 dir(),以及 globals()['mod'] 这种字典取值。

四个坑

  1. 只能在模块顶层写。 函数体、类体、try/except/finally 里出现 lazy 都是 SyntaxError;lazy from mod import * 和 lazy from __future__ import ... 也不允许。
  2. 报错时机被推迟到使用点。 模块不存在(或导入时抛异常)时,lazy import 语句本身不报错,错误在你第一次读这个名字时抛出,形状是 ModuleNotFoundError,__cause__ 是 ImportError: lazy import of 'x' raised an exception during resolution。好处是没装的可选依赖不再拖垮程序启动,坏处是报错位置离原因很远,try/except ImportError 也拦不住 import 语句。
  3. 导入的副作用一起被推迟。 注册表、插件装饰器、__init__.py 里的初始化都不会在启动时执行。如果某个框架靠 import 完成自动发现,惰性导入会静默地改变行为——这类场景要么别用,要么改成显式发现。
  4. 内省看不见它。 在第一次读取之前,模块不在 sys.modules 里,vars(proxy) 直接抛 TypeError。靠遍历 sys.modules 或依赖 import 副作用的工具(插件扫描、序列化框架、部分 mock 库)可能漏掉它。

什么时候值得用

PEP 810 的动机部分给出的经验值是启动时间降低 50–70%、内存降低 30–40%(这是 PEP 810 自己的说法)。它真正适合的是:

  • 启动路径很长、但大部分依赖只在个别子命令里用到的 CLI;
  • 冷启动敏感的 serverless 函数;
  • 重依赖(numpy、pandas、torch 之类)只是可选功能的情况。

要不要全量开 -X lazy_imports=all,建议先量一下:惰性导入只是把加载成本从“启动时”挪到“第一次用”,总量不会变,变的只是分布。如果程序启动后马上就要用到所有依赖,收益就是零,还要额外承担上面那几个坑。

参考:

扫码加入技术交流群🖱️
QR code