本文最后更新于:2025年12月18日 上午

Python 包功能多时按需加载很实用,本文记录 __getattr__ 实现类似功能的方法。

原理解析

__getattr__ 可以实现包的懒加载,它的核心思想是:当用户在模块中访问一个尚未定义的属性时,Python 会调用 __getattr__(name) 方法。我们可以在这里拦截请求,动态导入对应的模块。

  • 举例说明

以我的 vvdutils 库为例,这个库中包含了 MongoGridFSConnection 模块,用户可以调用 from vvdutils import MongoGridFSConnection 引用该功能,如果用 __getattr__ 的视角来看这个过程是这样的:

  1. 用户执行 from vvdutils import MongoGridFSConnection
  2. 如果 MongoGridFSConnection 不在 vvdutils 模块的全局变量中,Python 触发 __getattr__(‘MongoGridFSConnection’)
  3. 我们在 __getattr__ 中根据预设的映射表,找到 MongoGridFSConnection 实际位于 vvdutils.mongofs.connect 模块。
  4. 动态导入 vvdutils.mongofs.connect 模块,并从中取出 MongoGridFSConnection 类。
  5. 将这个类缓存vvdutils 模块的全局变量中,然后返回给用户。
  6. 下次再访问 MongoGridFSConnection 时,因为已在全局变量中,会直接返回,无需再次导入。

改造示例

示例说明

模块结构:

1
2
3
4
database
├── __init__.py
├── mongofs
└── mysql

其中 __init__.py 改造前为:

1
2
from .mongofs import *
from .mysql import *

该行为会导致每次导入库后会自动导入:

1
2
from .mysql.connect import MysqlConnection
from .mongofs.connect import MongoGridFSConnection

__getattr__ 懒加载改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# ./__init__.py
"""
vvdutils 主模块。
数据库连接类已启用懒加载,仅在首次访问时导入。
"""

# 1. 定义公开的接口,这有助于工具进行代码分析、自动补全和生成文档。
# 这里列出所有你希望用户从包顶级直接访问的类名。
__all__ = [
'MongoGridFSConnection', # 对应 mongofs.connect.MongoGridFSConnection
'MysqlConnection', # 对应 mysql.connect.MysqlConnection
# 未来可以轻松地在这里添加新的类名,例如:
# 'RedisConnection',
# 'PostgresConnection',
]

# 2. 核心:定义懒加载映射表。
# 格式:'对外暴露的属性名': ('模块路径', '实际类名')
# 模块路径相对于当前包(vvdutils),使用点号分隔。
_LAZY_IMPORTS = {
'MongoGridFSConnection': ('mongofs.connect', 'MongoGridFSConnection'),
'MysqlConnection': ('mysql.connect', 'MysqlConnection'),
# 未来添加新模块时,只需在此增加一行映射即可。
# 'RedisConnection': ('redis.connect', 'RedisConnection'),
}

# 3. 实现 __getattr__ 函数
def __getattr__(name: str):
"""
当试图访问本模块中不存在的属性时被调用。
用于实现类(尤其是重量级数据库连接类)的懒加载。
"""
if name in _LAZY_IMPORTS:
# 根据映射表获取模块路径和类名
module_path, class_name = _LAZY_IMPORTS[name]

# 动态导入模块。__import__ 返回顶级模块,getattr 逐级深入获取子模块。
# level=0 表示绝对导入。
module = __import__(module_path, fromlist=[class_name], level=0)

# 从模块中获取我们需要的具体类
klass = getattr(module, class_name)

# 【关键】将获取到的类缓存到本模块的全局变量中。
# 这样后续再次访问该属性时,Python会直接找到它,不会再触发 __getattr__。
globals()[name] = klass

return klass

# 如果请求的属性名不在我们的懒加载映射表中,则抛出标准的 AttributeError。
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

# 4. 实现 __dir__ 函数
def __dir__():
"""
返回模块中可用的属性名列表。
这对于 IDE 的自动补全和交互式环境(如 IPython)的 tab 补全至关重要。
"""
# 合并:内置属性、已加载的属性、以及我们声明支持懒加载但尚未加载的属性。
attrs = set(globals().keys()) # 当前已存在于全局作用域中的名称
attrs.update(_LAZY_IMPORTS.keys()) # 加上所有我们声明支持懒加载的名称
attrs.update(['__all__', '__doc__', '__getattr__', '__dir__']) # 确保特殊方法也在列表中
return sorted(attrs)

# 【可选但推荐】初始化提示
# 可以在这里导入一些轻量级的工具模块,它们不影响启动速度。
# 例如,如果你的 `utils` 模块很轻量,可以正常导入:
# from . import utils

# 注意:我们在这里并没有导入 mongofs 或 mysql,它们将在被访问时按需加载。

参考资料



文章链接:
https://www.zywvvd.com/notes/coding/python/python-getarrt-lazy-import/python-getarrt-lazy-import/


“觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭”

微信二维码

微信支付

支付宝二维码

支付宝支付

Python __getattr__ 懒加载
https://www.zywvvd.com/notes/coding/python/python-getarrt-lazy-import/python-getarrt-lazy-import/
作者
Yiwei Zhang
发布于
2025年12月18日
许可协议