Python 模块的魔法入口——__init.py__ 实战指南
我们在使用 python 库的时候会发现库内很多文件夹下都有一个__init__.py文件,有时候一个库里所有的代码都写在里面,有时候就是一个空文件,非常迷惑。
今天我们就来聊聊 Python 模块的入口__init__.py。

开门见山的说,__init__.py 的职责只有一个:把目录变成包,但“里面写不写东西、写多少”完全取决于你想让包长成什么样。
假设你的包名是 mypkg,下面给出一套“从简到繁”的最佳实践清单,按需取用即可。
- 最简单的空文件
什么都不写也能让import mypkg成功。适合一次性脚本或内部工具。
# mypkg/__init__.py(空文件即可)
- 暴露公共 API(99 % 的库都要)
# mypkg/__init__.py
from ._core import run_counter# 内部实现
from ._types import CounterResult # 公开的数据类
__all__ = ["run_counter", "CounterResult"] # 控制 from mypkg import * 的行为
__version__ = "1.2.0"# 方便 mypkg.__version__
要点
• 只在 __init__.py 里放“用户需要直接用的名字”,其余留作内部模块。
• 用 __all__ 明确导出,静态检查工具(mypy、pyright)也省心。
如果不配置
__all__和__version__会怎么样?只写这两行,包可以正常被 Python 识别,但会出现几个“副作用”:
- 任何 from mypkg import * 会一股脑把 run_counter 和 CounterResult 都导出去
因为没有 all,所以星号导入会把当前命名空间里所有非下划线开头的名字全带走(这里刚好就是那两个,所以没有影响)。多数情况下这不是你想要的,因为以后再加内部变量也会被导出。- IDE / 静态检查器(mypy、pyright)
它们依旧能找到 mypkg.run_counter,所以开发体验没问题;
但缺少 all 会让静态工具在“哪些符号是公有”这件事上给出更宽松的提示。- 没有 version 等元数据
用户无法import mypkg; print(mypkg.__version__),发布到 PyPI 时也不符合常见约定。- 没有懒加载、日志初始化等高级需求
如果包很小,这些可以忽略;中大型库通常需要。
- 延迟加载 & 子包懒导入(中大型库)
# mypkg/__init__.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# 仅给类型检查器看
from . import utils, io, plot
__all__ = ["run_counter", "plot"]
__version__ = "1.2.0"
# 运行时再真正 import,降低启动开销
def __getattr__(name: str):
if name == "run_counter":
from ._core import run_counter as rc
return rc
raise AttributeError(f"module {__name__} has no attribute {name}")
- 配置 logging、插件注册、元数据
# mypkg/__init__.py
import logging
from importlib.metadata import version
__version__ = version("mypkg")
# 默认给库打日志,但不污染用户根 logger
logging.getLogger(__name__).addHandler(logging.NullHandler())
# 自动注册插件(若需要)
from . import _plugins; _plugins.register()
- 可选:设置
__path__(高级)
当你想把一个大包拆成多个物理目录(namespace package),才需要去动 __path__,99 % 的情况不需要。
简单总结一下
__init__.py 就是包的“门面”。
• 只想让目录可导入 → 空文件。
• 想做库 → 至少写 __version__ 和公开 API。
• 想优化启动或做复杂结构 → 加懒导入、日志、插件注册等。

浙公网安备 33010602011771号