在此之前先確認一個概念是否弄清
模塊命名空間
1. 目錄結構
假設你有以下結構:
testpkg/__init__.pyfool.pymaybe.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.maybeprint(hasattr(testpkg, 'Fool')) # False
print(hasattr(testpkg, 'Maybe')) # Falseprint(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.maybeprint(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 testpkgprint(hasattr(testpkg, 'Fool')) # True
print(hasattr(testpkg, 'Maybe')) # Trueprint(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
里加