跳转到内容
新建笔记

Python 包、模块与导入

包把模块放进可导入的命名层次,例如 mypackage.module1。以下以 Python 3.11 为基线:常规包通常有 __init__.py,命名空间包则可以没有这个文件。包本身也是模块;安装工具使用的“分发项目”名称与 import 使用的包名也不必相同。

从五个文件理解包的接口

跳转到“从五个文件理解包的接口”

在一个工作目录中建立 mypackage 文件夹,把下面四段分别保存为注明的文件,并把第五段 check_package.py 放在 mypackage 旁边。不要把它们拼成一个脚本。

mypackage/module1.py:

def function1():
return "Function 1"
def _hidden_function():
return "explicit import still works"

mypackage/module2.py:这里的一个点指当前包。

from .module1 import function1
def function2():
return "Function 2 after " + function1()
if __name__ == "__main__":
print(function2())

mypackage/__init__.py:把常用函数放进包自己的命名空间,并给其中一个函数增加别名。

from .module1 import function1
from .module2 import function2
greet = function1
PACKAGE_LABEL = "demo"
__all__ = ["function1", "function2", "greet"]

mypackage/__main__.py:在工作目录运行 python -m mypackage 时执行这个入口。

from . import function1, function2
def main():
print(function1())
print(function2())
if __name__ == "__main__":
assert __package__ == "mypackage"
assert __spec__.name == "mypackage.__main__"
main()

check_package.py:在工作目录运行 python check_package.py,验证显式导入、星号导入与导入缓存。

import importlib
import mypackage
from mypackage import PACKAGE_LABEL, function1, function2
from mypackage.module1 import _hidden_function
assert function1() == "Function 1"
assert function2() == "Function 2 after Function 1"
assert mypackage.greet is function1
assert PACKAGE_LABEL == "demo"
assert _hidden_function() == "explicit import still works"
exported = {}
exec("from mypackage import *", exported)
assert set(exported) - {"__builtins__"} == set(mypackage.__all__)
assert "PACKAGE_LABEL" not in exported
assert "_hidden_function" not in exported
mypackage.runtime_marker = object()
marker = mypackage.runtime_marker
again = importlib.import_module("mypackage")
assert again is mypackage and again.runtime_marker is marker
assert mypackage.__package__ == "mypackage"
assert mypackage.__spec__.name == "mypackage"
assert mypackage.module1.__package__ == "mypackage"
assert mypackage.module1.__name__ == "mypackage.module1"
assert mypackage.module1.function1.__module__ == "mypackage.module1"
assert "PACKAGE_LABEL" not in vars(mypackage.module1)
print("package checks passed")

__init__.py 定义的 PACKAGE_LABEL 是包对象的属性,不会自动变成每个子模块的全局变量。空的 __init__.py 同样可以建立常规包;子包可采用相同结构。首次普通导入执行初始化代码,此后同一解释器中的常规重复导入通常使用 sys.modules 中的对象;显式重载、修改缓存或新的解释器会改变这个前提。

__all__ 只说明星号导入的接口

跳转到“__all__ 只说明星号导入的接口”

上例在 __init__.py 中导入函数,是为了支持 mypackage.function1 和 from mypackage import function1。__all__ 进一步列出 from mypackage import * 的名称;它不会禁止显式导入 PACKAGE_LABEL,也不会把下划线开头的函数变成不可访问的私有成员。

没有 __all__ 时,星号导入通常取得当前命名空间中不以下划线开头的名称。对包而言,这不意味着遍历目录并自动导入所有子模块;已经加载的子模块可能已经成为包属性。维护公共接口时,显式导入通常更容易追踪名称来源。规则见 Python 模块教程的包导入说明。

相对导入、入口与循环依赖

跳转到“相对导入、入口与循环依赖”

包内部的 from .module1 import function1 从当前包开始查找;from ..shared import value 从上一级包开始。相对导入需要包上下文。直接运行上面的 python mypackage/module2.py 会因为缺少父包上下文而失败。可以从工作目录用 python -m mypackage 运行包入口。若执行 python -m mypackage.module2,这个例子的 __init__.py 已经预先导入了 module2,会触发重复执行相关的警告;要把某个子模块单独作为命令入口,应避免在包初始化时先导入它。入口规则见 Python 的 __main__ 文档。

把更多导入放进 __init__.py 并不能消除循环依赖。假如 a.py 在定义 VALUE 前就从 b.py 导入函数,而 b.py 又立即从 a.py 导入 VALUE,后者看到的可能是尚未初始化完的模块。优先把共享常量或接口移到第三个模块,让两边都依赖它;确实需要延迟依赖时再在调用点导入,并检查运行顺序。包初始化适合少量必要的接口组织,不适合隐式启动耗时任务。

命名空间包可以分布在多个目录

跳转到“命名空间包可以分布在多个目录”

下面的独立程序临时创建两个不同的搜索路径,每个路径贡献 demo_namespace_parts 的一个模块。两个包目录都没有 __init__.py,但导入后属于同一个命名空间包。程序结束时恢复搜索路径并移除自己加载的演示模块。

import importlib
from pathlib import Path
import sys
from tempfile import TemporaryDirectory
package_name = "demo_namespace_parts"
original_path = sys.path.copy()
assert not any(
name == package_name or name.startswith(package_name + ".")
for name in sys.modules
)
with TemporaryDirectory() as directory:
root = Path(directory)
first, second = root / "first", root / "second"
for folder in (first, second):
(folder / package_name).mkdir(parents=True)
(first / package_name / "left.py").write_text("VALUE = 10\n", encoding="utf-8")
(second / package_name / "right.py").write_text("VALUE = 20\n", encoding="utf-8")
try:
sys.path[:0] = [str(first), str(second)]
importlib.invalidate_caches()
package = importlib.import_module(package_name)
left = importlib.import_module(package_name + ".left")
right = importlib.import_module(package_name + ".right")
assert left.VALUE + right.VALUE == 30
assert {Path(item) for item in package.__path__} == {
first / package_name, second / package_name
}
assert package.__spec__.origin is None
assert all(not (folder / package_name / "__init__.py").exists()
for folder in (first, second))
finally:
sys.path[:] = original_path
for name in list(sys.modules):
if name == package_name or name.startswith(package_name + "."):
sys.modules.pop(name)
importlib.invalidate_caches()
print("namespace package checks passed")

这里成立的条件是两个根目录都进入了导入搜索路径。不是任何没有 __init__.py 的文件夹都会自动成为可导入的包;同名常规包还会影响查找结果。命名空间包机制从 Python 3.3 开始提供,完整规则见 PEP 420。

文件、模块与对象的特殊名称

跳转到“文件、模块与对象的特殊名称”

这些名称分属不同对象,不能一概叫“模块变量”。下面集中列出原笔记涉及的元数据;运行时方法放在特殊方法与对象协议。

名称所属对象与用途使用边界
__init__.py、__main__.py常规包初始化文件、包的命令入口文件文件名;不同于实例方法 __init__
__all__模块或包列出的星号导入名称不限制显式访问
__future__支持 future 语句的标准库模块部分特性改变编译行为;不是把当前解释器升级为未来版本
__name__模块的导入名称,或主程序的 "__main__";函数、类也有各自的名称要先分清查询的是哪个对象
__file__从文件加载的模块通常记录来源路径可不存在;不宜假定所有模块都来自磁盘文件
__doc__模块、函数、类等对象的文档字符串没有文档字符串时通常为 None
__package__用于相对导入的包名Python 3.11 中普通顶层导入模块通常为 "";直接运行脚本可能为 None
__loader__模块加载器对象负责加载,不等同于模块的说明对象
__spec__模块的 ModuleSpec,记录名称、加载器等导入信息直接运行文件的 __main__ 可为 None
__qualname__函数或类在定义上下文中的限定名,如 Outer.Inner不包含模块名前缀;与 __module__ 配合定位定义
__annotations__模块、函数、类的注解映射不是运行时类型验证;启用延迟注解时其值可能是字符串
__module__函数、类等对象定义时所属的模块名不是每个对象都具有的统一接口
__bases__类的直接基类元组不等于完整的方法解析顺序
__class__普通对象所属的类判断实际类型通常使用 type(obj);不要与模块名混淆
__dict__模块或普通实例的属性映射;类上通常是只读的映射视图某些对象没有它,采用 __slots__ 的实例就是常见情况

下面的完整程序区分模块名与限定名,并观察 Python 3.11 的延迟注解。

from __future__ import annotations
from types import MappingProxyType, ModuleType
class Outer:
class Inner:
count: int
def __init__(self, count: int):
self.count = count
sample = Outer.Inner(3)
assert Outer.Inner.__qualname__ == "Outer.Inner"
assert Outer.Inner.__module__ == __name__
assert Outer.Inner.__bases__ == (object,)
assert Outer.Inner.__annotations__ == {"count": "int"}
assert isinstance(Outer.Inner.__dict__, MappingProxyType)
assert sample.__dict__ == {"count": 3}
memory_module = ModuleType("memory_demo")
assert memory_module.__name__ == "memory_demo"
assert not hasattr(memory_module, "__file__")
print("metadata checks passed")

导入相关属性见 Python 导入系统;future 特性的生效版本与强制启用版本见 __future__ 文档。名称与作用域可接着阅读作用域与变量绑定。