在此之前先确认一个概念是否弄清
模块命名空间
1. 目录结构
假设你有以下结构:
testpkg/
__init__.py
fool.py
maybe.py
内容如下:
fool.py
# testpkg/fool.py
class Fool:
pass
maybe.py
# testpkg/maybe.py
class Maybe:
pass
__init__.py
(先什么也不写,空文件)
2. 只import子模块,不动__init__.py
在外部写测试代码:
import testpkg.fool
import testpkg.maybe
print(hasattr(testpkg, 'Fool')) # False
print(hasattr(testpkg, 'Maybe')) # False
print(hasattr(testpkg.fool, 'Fool')) # True
print(hasattr(testpkg.maybe, 'Maybe')) # True
解释:
testpkg.fool
模块的命名空间里有Fool
(因为在fool.py里定义了)testpkg.maybe
模块的命名空间里有Maybe
- 但是
testpkg
(这个包)的命名空间里没有Fool
和Maybe
,因为没把它们导入到包的顶层。
3. “模块命名空间”到底是什么?
- 每个py文件(模块)加载后,Python会创建一个名字空间(实际是一个dict),存放所有在这个模块里定义的名字。
- 可以通过
模块名.__dict__
看到它的命名空间。
例如:
import testpkg.fool
import testpkg.maybe
print(testpkg.fool.__dict__.keys())
print(testpkg.maybe.__dict__.keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'Fool'])
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'Maybe'])
4. 只有import到包,名字才会出现在包的命名空间
如果你修改testpkg/__init__.py
为:
from testpkg.fool import Fool
from testpkg.maybe import Maybe
__all__ = ["Fool", "Maybe"]
- 当
import testpkg
的时候,实际上会执行testpkg/__init__.py
里的代码。 from testpkg.fool import Fool
这行代码的作用是:
把testpkg.fool
这个模块里的Fool
,导入到当前命名空间(也就是testpkg
的命名空间)下。- 同理,
from testpkg.maybe import Maybe
把Maybe
导入到testpkg
命名空间下。
现在再测试:
import testpkg
print(hasattr(testpkg, 'Fool')) # True
print(hasattr(testpkg, 'Maybe')) # True
print(testpkg.__dict__.keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__path__', '__file__', '__cached__', '__builtins__', 'fool', 'Fool', 'maybe', 'Maybe', '__all__'])
此时Foo
和Bar
被“绑定”到了testpkg
这个包的命名空间下。
补充个__all__
讲解
__all__
是一个导出控制列表,是一个字符串列表。
当你这样用的时候:
from testpkg import *
Python 会只导入 __all__
里列出的名字。
也就是说:
- 如果
__init__.py
里有__all__ = ['Fool', 'Maybe']
, - 那么
from testpkg import *
只会导入Fool
和Maybe
, - 其他即使你在
__init__.py
里定义了,也不会随*
导出。
注意
__all__
只影响from ... import *
这种用法。- 普通的
import testpkg
不受影响。
非常好,你对模块命名空间、包、以及 __init__.py
的作用已经理解得很清楚了!下面我会在你的基础上,进一步系统性讲解 __init__.py
文件,帮助你更深入理解它的地位与用法。
__init__.py
文件
一、__init__.py
的本质
__init__.py
是 包的初始化脚本。- 只要目录下有
__init__.py
,Python 才会把这个目录当做一个包(Python 3.3+ 以后支持“隐式命名空间包”,但强烈建议有__init__.py
,便于兼容和控制行为)。 - 当你
import testpkg
时,Python 实际执行testpkg/__init__.py
,并把里面的内容放进testpkg
这个模块(包)对象的命名空间。
二、__init__.py
的作用
1. 标识包
- 没有
__init__.py
,Python 2 不认这个目录是包,会报错。 - Python 3.3+ 的确可以没有(隐式包),但有了
__init__.py
能更清楚、兼容、可控。
2. 包初始化
- 你可以在
__init__.py
里写包初始化逻辑,比如设置全局变量、初始化状态、打印调试信息等。
3. 控制包的“顶层接口”(API 设计)
- 你可以在
__init__.py
里导入(或重新命名)子模块、类、函数,让用户用更简单的方式访问包内容。 - 例如:
这样用户可以# testpkg/__init__.py from .fool import Fool from .maybe import Maybe
from testpkg import Fool
,不用知道子模块结构。
4. 控制 from testpkg import *
导出的内容
- 通过定义
__all__
列表,决定哪些名字会被*
导出。__all__ = ['Fool', 'Maybe']
5. 可以导入子包、子模块
- 你可以在
__init__.py
里导入子包、子模块,甚至重命名,隐藏实现细节。from . import fool as _fool
三、实践举例
1. 最简单的情况
# 空文件
- 只起到“标识包”的作用。
2. 聚合包的接口(对外API)
# testpkg/__init__.py
from .fool import Fool
from .maybe import Maybe
__all__ = ['Fool', 'Maybe']
- 这样
testpkg.Fool
、testpkg.Maybe
就变成包的“顶层接口”。
3. 初始化逻辑
# testpkg/__init__.py
print("testpkg包被导入了!")
_config = {"debug": True}
- 导入包时会输出一句话,设置包级别的配置。
4. 导出模块而不是类
# testpkg/__init__.py
from . import fool
from . import maybe
__all__ = ['fool', 'maybe']
- 这样
from testpkg import fool
就可以直接用testpkg.fool.Fool
。
四、几点注意
相对导入和绝对导入
from .fool import Fool
:点号代表“当前包”,推荐包内部用相对导入。from testpkg.fool import Fool
:绝对导入,避免循环依赖出错。
循环引用问题
- 包内部如果互相引用,要注意不要在模块顶层出现死循环引用,否则会报
ImportError
。
- 包内部如果互相引用,要注意不要在模块顶层出现死循环引用,否则会报
__init__.py
并不是必须的,但建议始终写上- 这样会让代码更清晰,有更好的兼容性。
包结构的“封装”思路
- 你可以只暴露有限的接口给用户(通过
__init__.py
),隐藏实现细节。
- 你可以只暴露有限的接口给用户(通过
五、你可以这样理解
- 包(package) = 文件夹 +
__init__.py
- 包的命名空间 =
__init__.py
的命名空间 - 你让包有哪些“顶级名字”,就在
__init__.py
里加