包把模块放进可导入的命名层次,例如 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 function1from .module2 import function2
greet = function1PACKAGE_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 importlibimport mypackagefrom mypackage import PACKAGE_LABEL, function1, function2from mypackage.module1 import _hidden_function
assert function1() == "Function 1"assert function2() == "Function 2 after Function 1"assert mypackage.greet is function1assert 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 exportedassert "_hidden_function" not in exported
mypackage.runtime_marker = object()marker = mypackage.runtime_markeragain = 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 importlibfrom pathlib import Pathimport sysfrom 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__ 文档。名称与作用域可接着阅读作用域与变量绑定。