模块、包与导入系统
本节目标
查询 Python 模块对象、包、导入查找、sys.modules、importlib 和循环导入。
模块把一份 Python 代码和它的全局命名空间组织成可导入的对象;包为带有子模块的导入命名空间。本文以 模块教程、导入语句参考、importlib 文档、sys.modules 文档 和 runpy 文档为准。异常处理可接续异常、上下文管理器与资源管理,异步模块的入口语义可接续async / await、异步迭代与异步上下文。本文讨论语言和导入系统本身,不展开项目分发流程。
模块对象、命名空间与首次执行
导入语句参考中,一个模块是有独立命名空间的对象。首次导入会建立该对象并执行模块顶层代码;执行所得的全局名称保留在模块字典中。__name__ 是这份执行环境中的名称,不是源文件的唯一身份。
import settings
print(settings.__name__)
顶层语句因此可能产生副作用。被导入的模块应把可复用定义放在顶层,并谨慎安排只有入口执行时才需要的动作。
import 的查找与名称绑定
导入语句参考规定 import 要先解析并加载目标,再把结果按语句形式绑定到当前作用域。导入查找和把名称绑定到当前作用域是两个独立操作:缓存命中可以不执行新模块代码,但当前作用域仍会得到新绑定。
import course.notes
from course import notes
第一种形式在当前作用域绑定 course,第二种形式绑定 notes。不要从绑定结果倒推查找过程,也不要把名称遮蔽误判为模块没有被导入。
sys.path、finder、loader 与 module spec
importlib 文档把导入机制分成查找和加载。路径型查找器会依据 sys.path 或包的 __path__ 找候选项;finder 产生 ModuleSpec,loader 按该 spec 创建或执行模块。sys.path 是查找输入之一,不是已经导入的模块清单。
import importlib.util
spec = importlib.util.find_spec("json")
排查导入时先确认目标名、包上下文和查找路径,再观察得到的 spec;不要把语言级导入规则和项目目录约定混为一谈。
sys.modules 缓存
sys.modules 文档是已加载模块的缓存。模块会在执行其代码前插入 sys.modules,这让递归导入能取得同一个模块对象。若首次加载失败,失败的模块会从 sys.modules 移除,但副作用导入的模块可能保留;因此失败并不等于进程状态完全回滚。
import importlib
import runpy
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
prefix = "workshop"
saved_path = list(sys.path)
saved_dont_write_bytecode = sys.dont_write_bytecode
try:
with TemporaryDirectory() as directory:
root = Path(directory)
package = root / prefix
package.mkdir()
(package / "__init__.py").write_text("NAME = 'workshop'\n", encoding="utf-8")
(package / "core.py").write_text("NAME = 'core'\n", encoding="utf-8")
(package / "report.py").write_text("from .core import NAME\n", encoding="utf-8")
state_path = package / "state.py"
state_path.write_text("VERSION = 1\n", encoding="utf-8")
entry_path = root / "entry.py"
entry_path.write_text("RESULT = __name__\n", encoding="utf-8")
sys.dont_write_bytecode = True
sys.path.insert(0, directory)
imported = importlib.import_module(prefix)
cached = importlib.import_module(prefix) is imported
report = importlib.import_module(f"{prefix}.report")
state = importlib.import_module(f"{prefix}.state")
state_path.write_text("VERSION = 2\n", encoding="utf-8")
importlib.invalidate_caches()
reloaded = importlib.reload(state)
main_name = runpy.run_path(str(entry_path), run_name="__main__")["RESULT"]
print(f"package={imported.NAME}")
print(f"cache={cached}")
print(f"relative={report.NAME}")
print(f"reload={reloaded.VERSION}")
print(f"dynamic={report.__name__}")
print(f"main={main_name}")
finally:
sys.path[:] = saved_path
sys.dont_write_bytecode = saved_dont_write_bytecode
for name in list(sys.modules):
if name == prefix or name.startswith(f"{prefix}."):
del sys.modules[name]
package=workshop
cache=True
relative=core
reload=2
dynamic=workshop.report
main=__main__
示例临时把最小包加入 sys.path,第二次导入取得相同对象,并在 finally 中恢复路径、字节码开关和该包的缓存项。脚本本身不把临时导入遗留到调用进程。
包、__init__.py 与子模块
模块教程中的普通包通常用 __init__.py 标识,并在导入包时执行该文件;它可定义包级名称或初始化动作。Python 也支持不含 __init__.py 的命名空间包,所以不能声称所有包都必须有该文件。
import workshop.report
print(workshop.report)
成功导入子模块后,导入系统会把它作为属性放到父包对象上。包的初始化代码应尽量轻量,避免把可选子模块和全局副作用强行绑定在每次导入上。
绝对导入与显式相对导入
导入语句参考默认采用绝对导入:名称从顶层命名空间解析。显式相对导入用前导点从当前包定位,例如 from .core import NAME;显式相对导入依赖包上下文,直接把子模块作为普通脚本执行时通常没有该上下文。
from workshop.core import NAME
from .core import NAME as local_name
选择相对导入时要让代码由包导入或模块入口执行;不要依靠当前工作目录让直接执行偶然成功。
__main__ 与入口边界
runpy 文档说明可在受控的全局命名空间中执行代码。作为顶层入口执行的代码,__name__ 会是 __main__;被导入时则是其可导入名称。入口保护把这两种用途分开。
if __name__ == "__main__":
main()
模块作者应让导入只提供定义,把运行命令留给入口分支;同一文件既被导入又作为入口运行时,两个模块对象和其状态不能假定相同。
importlib、动态导入与 reload
importlib 文档提供 import_module()、缓存失效和 reload() 等接口,适合明确需要运行时按名称加载的场景。reload() 会保留模块字典,并重新执行模块代码覆盖或补充名称;未重新定义的旧名称可能继续存在。
import importlib
module = importlib.import_module("workshop.report")
importlib.reload(module)
from module import name 得到的外部引用不会自动重新绑定。重载不等于全局状态重置,调用方应显式读取重载后的模块属性或设计可替换的边界。
循环导入与部分初始化
导入语句参考所述的提前缓存让循环导入能够终止递归,但也意味着另一模块可观察到尚未执行完的模块。此时访问后面才会定义的属性可能失败,问题核心是初始化顺序,不是缓存随机失效。
# a.py imports b; b.py imports a
优先把共享定义提取到第三个模块、把导入移入真正需要的局部范围,或反转依赖方向。延迟导入只能在确实消除初始化环时使用,不能掩盖错误的模块职责。
导入副作用、缓存与执行入口
本地同名文件可能遮蔽预期模块,顶层导入会在首次执行时产生副作用,reload() 又不会自动更新其他模块早已持有的引用。模块教程和 importlib 文档给出了一条检查线索:实际加载的模块、执行时点,以及已复制到其他作用域的名称。
import json
print(json.__file__)
诊断时可从实际模块的 __file__、__spec__ 和 sys.modules 项开始,再缩小顶层副作用并分离入口代码。模块系统只承担名称、查找、加载和缓存,项目的发布与依赖策略仍是另一层问题。