如何动态定义包级__all__,且不执行模块中的耗时代码?
如何读取模块的__all__定义并动态添加到包级__init__.py,同时避免执行模块耗时代码?
问题场景
我正在开发一个Python库,包结构如下:
library/ package1/ __init__.py # 子包__init__文件 _module_a.py _module_b.py package2/ __init__.py # 包级__init__文件 subpackage/ __init__.py # 子包__init__文件 _module_d.py _module_e.py _module_f.py _module_g.py __init__.py # 库级__init__文件
所有模块都加了下划线前缀,用来严格控制dir()输出的内容,每个模块都定义了静态的__all__列表。但模块中包含耗时代码(比如time.sleep(5)),如果直接导入模块会执行这些代码,影响性能。
示例模块内容:_module_e.py:
"""_module_e.py文件内容""" import time __all__ = ["Foo", "Bar"] # 执行计算密集型操作 time.sleep(5) class Foo: pass class Bar: pass
_module_f.py:
"""_module_f.py文件内容""" import time __all__ = ["Baz"] # 执行更多耗时操作 time.sleep(5) class Baz: pass
现在希望用户可以用from library.package2.subpackage import Baz这种简洁方式导入,而不是直接导入下划线前缀模块,但又不想手动维护__init__.py中的映射关系(比如手动写defined_classes字典)。
解决方案:静态解析模块文件提取__all__,自动生成映射
可以利用Python的ast模块静态解析模块文件,提取__all__列表内容,自动生成名称到模块的映射,结合__getattr__实现按需动态导入,完全不需要手动维护映射,也不会执行模块的耗时代码。
修改subpackage/__init__.py如下:
import ast import importlib from pathlib import Path # 自动生成名称到模块的映射 defined_classes = {} package_dir = Path(__file__).parent # 遍历当前目录下的下划线前缀模块文件 for module_path in package_dir.glob("_module_*.py"): module_name = module_path.stem with open(module_path, "r", encoding="utf-8") as f: # 解析模块的抽象语法树,不执行代码 tree = ast.parse(f.read()) # 查找__all__的赋值语句 for node in ast.walk(tree): if (isinstance(node, ast.Assign) and any(target.id == "__all__" for target in node.targets)): # 解析__all__的字面量列表 try: all_names = ast.literal_eval(node.value) for name in all_names: defined_classes[name] = module_name except (ValueError, TypeError): # 如果__all__不是静态字面量列表,跳过该模块 continue break # 找到__all__后停止遍历当前模块的AST # 定义包的__all__,控制dir()输出 __all__ = list(defined_classes.keys()) def __dir__(): return __all__ def __getattr__(name): if name in defined_classes: # 按需导入对应的模块,此时才会执行模块代码 module = importlib.import_module(f".{defined_classes[name]}", __name__) return getattr(module, name) raise AttributeError(f"Module '{__name__}' has no attribute '{name}'")
关键说明
- 静态解析安全高效:用
ast模块解析文件,不会执行任何模块代码,只会提取__all__的静态定义,完全避免了耗时代码的提前执行。 - 自动维护映射:无需手动更新
defined_classes字典,新增或修改模块的__all__后,__init__.py会自动识别。 - 按需导入:只有当用户实际访问某个名称时,才会导入对应的模块并执行其代码,做到真正的懒加载。
- 兼容性:如果模块的
__all__是动态生成的(比如不是字面量列表),该方法会跳过解析,这种情况可以单独处理,但对于静态定义__all__的场景完全适用。
内容的提问来源于stack exchange,提问作者Yaseen
相关产品推荐
相关产品推荐

