Python 導包技巧
起因
起初學 Go 語言的時候,對它的導包規則感到痛苦,——“那麼麻煩幹嘛呢!”。但最近接到一些新功能開發任務,於是 “啪嗒啪嗒” 寫代碼,結果更痛苦了。公司項目目錄規劃打一開始就不合理,以此爲基礎的 “繁榮發展” 導致更多混亂與麻煩。於是馬上翻開《Python cookbook》解惑,有些心得,這裏記之。
場景1
場景1:如何模塊化代碼,同時保證導入方式統一?
公司項目是老項目,因此代碼祖傳,常常一個 .py 文件裏有上千行代碼。恰巧現在重構(雖然並不是嚴格意義上的重構),同事們有意識地將代碼拆分成多個文件。然後就有了這樣的現象:
from onedir.twodir.file1 import func1
from onedir.twodir.three.file2 imort func2
from onedir.file3 import func3
目錄結構如下:
Guan@Orchard:~/python/08/demo$ tree onedir/
onedir/
├── file3.py
├── __init__.py
└── twodir
├── file1.py
├── __init__.py
└── three
├── file2.py
└── __init__.py
2 directories, 6 files
很顯然,要我記住哪個函數在哪個模塊中這是很困難、又沒意義的事。況且 onedir 裏邊的代碼只有一個模塊在使用,所以爲什麼不讓調用方式統一一下呢?這裏需要用到 Python 包中的 __init__.py 文件。
現在,修改 onedir 目錄下的 init 文件,修改後變成下面這樣:
# onedir/__init__.py
from .file3 import *
from .twodir.file1 import *
from .twodir.three.file2 import *
利用 init 文件加載子模塊,使得子模塊中的代碼在導入父級包時就可以直接使用。示例如下:
"""原來的導入方式"""
# from onedir.twodir.file1 import func1
# from onedir.twodir.three.file2 import func2
# from onedir.file3 import func3
""""現在的導入方式"""
from onedir import func1, func2, func3
func1()
func2()
func3()
# 輸出:
func1
func2
func3
這裏額外補充一下相對路徑導入模塊的姿勢。
我們都知道,在執行 import xxx
語句時,解釋器會先在當前目錄下找有沒有這個模塊,如果沒有,就去 sys.path 中的路徑裏去找。當兩個 .py 文件在同級目錄時:
Guan@Orchard:~/python/08$ tree demo/
demo/
├── view1.py
└── view2.py
0 directories, 3 files
在 view2.py 文件中可以直接 import view1
,但是不可以 import .view1
。因爲相對路徑只允許 from … import … 這種導入方式。所以是不是 from . import view1 就可以了呢?也不行!
爲控制篇幅,我主要是在這裏拋結論,想獲悉原理可以耐心去看看這篇文章:Python相對導入機制詳解 。
結論:
- 在執行腳本所在的目錄下,不能使用相對路徑的導入方式。 也就是說,如果我想在 demo 目錄下運行 view2.py 文件,裏邊還有相對導入,那是絕對不可以的。當這個目錄不是執行腳本所在目錄時,就可以用相對導入了。
- 如果堅持直接運行 view2.py ,又想使用相對導入,請以
python -m demo.view2
這種方式運行腳本(這種方式下,Python2 要求 demo 目錄中存在 init 文件,但 Python3 沒有此要求)。
場景2
場景2:如何控制符號(函數,變量等)的導入?
我們現在已知的是,當一個函數名(變量、類同理)以下劃線開頭時,不允許 from xxx import *
的方式導入,但可以 from xxx import _xxx
這樣導入。但不知道你們有沒有覺得,下劃線開頭的符號看上去有點醜,我很不愛用,那麼有沒有其他控制方式呢?那當然是有的:__all__
。
在一個模塊中定義變量 __all__
,並指明允許導出的符號名,from ... import *
就不能導入沒有列出的那些符號了。用法如下:
def func1():
pass
def func2():
pass
__all__ = ["func1"] # 在其他模塊中使用時,func1 可以被導入,func2 不行
當然,from xxx import func2
仍可以把 func2() 導進來。
場景3
場景3:如何讓不同目錄下的代碼在統一的命名空間中導入?
目錄結構如下:
Guan@Orchard:~/python/08/demo$ tree demo/
demo/
├── demo1
│ └── spam
│ └── a.py
└── demo2
└── spam
└── b.py
4 directories, 2 files
現在,需要使用 a.py 和 b.py 中的代碼,尋常方式是:
import demo1.spam.a
import demo2.spam.b
儘管它們都在 spam 目錄下,但因爲頂級目錄(demo1 和 demo2)不同,所以需要不同的入口。但其實,是可以統一 a.py 和 b.py 的入口的。解決方式如下:
import sys
sys.path.extend(["demo1", "demo2"])
import spam.a
import spam.b
事實上,此時的 spam 是一個命名空間包。現在打印 spam ,得到的結果會是:<module ‘spam’ (namespace)>。同時,命名空間包也不會有 __file__ 這個屬性。
...
In [5]: spam
Out[5]:<module 'spam' (namespace)>
In [6]: spam.__file__
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-b25cc88cc23e> in <module>()
----> 1 spam.__file__
AttributeError: module 'spam' has no attribute '__file__'
創建命名空間包的關鍵在於,統一命名空間的頂層目錄中不能有 __init__.py 文件。拿上面的例子來說,就是不能在 spam 目錄下有這個文件。
你可以嘗試導入一個目錄,在兩種情況下,一個是該目錄下存在 init 文件,一個是不存在這個文件。然後打印這個目錄,看看結果會有什麼不同。
感謝
- 參考 《Python cookbook(第三版)》
- 參考 Python相對導入機制詳解