bigywater.tech
Python · 32

__init__.py 和包加载:一个包是怎么被导入的

真实构造一个带__init__.py的包,验证import包时__init__.py的执行时机、__all__对星号导入的限定作用,以及完全不写__init__.py的命名空间包依然能被真实导入。

import包先执行__init__.py
真实带出from .util import helper触发的子模块加载
__all__限定星号导入
真实_private和子模块本身都被挡在外面
无__init__.py也能导入
真实PEP 420命名空间包,__file__变成None

做的事:给一个包写__init__.py,看import这个包到底执行了什么

代码 pkg1/__init__.py / pkg1/util.py# pkg1/__init__.py print("pkg1/__init__.py: executing") from .util import helper VERSION = "1.0" __all__ = ["helper", "VERSION"] # pkg1/util.py print("pkg1/util.py: executing") def helper(): return "helper called" def _private(): return "should not leak via *" import pkg1 print(pkg1.__file__) print(pkg1.VERSION) print(pkg1.helper())

验证:import一个包,真实先跑的是__init__.py

实测 · 本机执行输出
pkg1/__init__.py: executing     ← 真实先执行__init__.py
pkg1/util.py: executing          ← __init__.py里的from .util import helper真实触发util.py执行

pkg1.__file__     → .../pkg1/__init__.py   ← 包的__file__真实指向__init__.py本身
pkg1.VERSION      → 1.0
pkg1.helper()     → helper called

真实的顺序完全对应__init__.py里代码的写法:import pkg1触发的第一件事就是执行pkg1/__init__.py这份文件,它里面的from .util import helper又真实触发了util.py被加载一遍——所以pkg1.helper能直接用,本质上是__init__.py把子模块里的名字"搬"到了包自己的命名空间里,调用者完全不需要知道helper原本定义在util.py里。

验证:__all__真实决定了from pkg import *能看到谁

实测 · 本机执行输出
from pkg1 import *

"helper" in dir()    → True    ← 在__all__里
"_private" in dir()  → False   ← 不在__all__里,即使util.py里定义了它
"util" in dir()      → False   ← util子模块本身也不在__all__里,星号导入不会带进来
这组对比真实验证了__all__的作用范围:from pkg1 import *只会把__all__列表里明确写出来的名字带到调用者的命名空间——helper在列表里,真实能用;_private虽然在util.py里定义、甚至可以通过pkg1.util._private直接访问到,但星号导入真实完全看不到它;连util这个子模块本身(尽管它已经因为from .util import helper被真实加载并挂在pkg1命名空间下)也没有出现在星号导入的结果里,因为它不在__all__清单上。没写__all__的包,星号导入的默认行为是带走所有不以下划线开头的顶层名字——这也是为什么很多项目的代码规范要求"库代码不要用import *",它的可见范围完全由被导入方的__all__控制,调用方读代码时无法只看这一行就知道到底引入了哪些名字。

验证:完全不写__init__.py,包真实照样能被导入(PEP 420)

实测 · 本机执行输出
目录结构:nspkg/mod_ns.py(真实没有__init__.py)

import nspkg.mod_ns as m
print(m.X)                       → namespace works    ← 真实成功导入

import nspkg
print(nspkg.__file__)            → None    ← 真实没有__file__,因为没有实体的__init__.py文件
print(list(nspkg.__path__))      → ['.../nspkg']    ← 但__path__真实存在,指向这个目录本身

这是PEP 420在Python 3.3之后真实内置的行为:只要一个目录下有Python文件、但没有__init__.py,Python的默认查找器依然会真实把它当成一个"命名空间包"来对待——nspkg.mod_ns真实能正常导入,只是nspkg本身没有一份真正的__file__(它不对应任何一份被执行过的源文件),取而代之的是一个__path__,指向这个目录,供后续查找子模块用。这个特性真实常被用在"插件式"的项目结构里:多个独立分发的包可以共享同一个顶层命名空间(比如company.plugin_a和company.plugin_b分别打包发布,运行时它们的company目录会真实被合并成同一个命名空间包),代价是行为比显式__init__.py更隐晦——包本身没有一处集中的地方能放初始化逻辑或者__all__声明。

分步演示:包加载真实经历的三种情形

每一步都是本文上面用真实构造的包目录跑出来的原始结果。

从__init__.py执行到__all__限定再到无__init__.py的命名空间包
这一步新验证的行为

    本文用到的工具

    • 本机 Python 3.14.7,真实构造的带__init__.py的包和不带__init__.py的命名空间包
    京ICP备2026067502号-1京公网安备11010502063487号