You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何动态定义包级__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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 02:55:56