Python - import#
當 code 變多,需要更好的整理和封裝,因此 python 提供了 package、module 的功能, 及 import 的語法來調用這些包裝好的元件。
python 中,一個 .py 的檔案即為一個 module。而存放 module 的目錄,則稱為一個 package。
在 import 過程中,module 會被編譯、執行,並在 import 後,
被放入 caller 的 local symbol table 當中。
之後可透過 module.symbol 來調用這個 module 下的 symbol。
如此簡單、似是而非的描述,距離實際上實作細節還很遙遠。 在深入一個個機制之前,先列一下希望能回答的問題。
- package 與 module 的定義是什麼?有什麼分別嗎?責任的界定是什麼呢?
- 傳說在 package 的目錄當中,必須要有一個
__init__.py的存在?這是做什麼用的? - 我的 python interpreter 去哪邊找 package 的,什麼是
sys.path? - 這些package、module 是一個物件嗎?是的話他們的生命週期是怎樣呢?
- 會不會有 circular import 的發生?有什麼限制?
import 的基本語法#
在 python3 - module (or python2 - module) 中,介紹得滿清楚的。複習一下這些內容:
import mod- 最簡單的型式,找到 module 後,在 local symbol table 中建立 (bind) 名為
mod的變數,reference 到 modulemod。
import pkg.mod- 找到 package
pkg後,找到 modulemod,在 local symbol table 中建立pkg。之後可以透過pkg.mod.X去調用下面的東西。
import pkg.mod as mod- 匯入後改變在 local symbol table 中的名稱。在
pkg.modimport 後,可用mod去存取。
from pkg import mod、from pkg.mod import sym- 使用 import-from 的形式,跟上一個差不多,但是除了從 package 中 import module,還多了可以從 module 中 import symbol 的功能。
from pkg import mod as m- import-from 的形式也可以使用 as 來改變名稱。可用來避免名稱的衝突。
from . import mod- 從目前所在的 module,import 另外一個在同 package 的 module。這樣做可以避免在 coding 時綁定 package name,如果之後 code 還會有搬動時不需要全部更改。
from .. import mod、from ..package2 import mod- 不只是同一層的 package,relative import 可以描述任何相對的 package 位置。但不能超出最上層 package 的範圍就是了,不然會得到
ValueError: Attempted relative import beyond toplevel package
組合百百種,沒提到的部分直接看 python3 - grammar,
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+)
'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*
實例上,如考慮目錄結構:
pkg/
+-- __init__.py
+-- mod.py
這樣的目錄結構就是一個 regular package,在 import pkg.mod 時,
會先處理 pkg 這個 module,去編譯、執行 pkg/__init__.py,接著換 pkg/mod.py。
大致來說,整個 import 的流程會有
- 搜尋 (find) - 怎麼找到
pkg/,怎麼找到pkg/mod.py - 編譯、執行
- 將 reference 塞入 symbol table 當中 (binding)
並對於像 pkg.mod 這樣的巢狀結構,會 recursive 的處理所有經過的 package 或 module。
script or module?#
在這邊所說的 script,指得是當我們在執行 python script.py 的那個 script.py 檔案。
如果說每一個 .py 都可以是一個 module,那 script 與 module 有沒有什麼不同呢?
如果沒有不同的話,import 進來跟執行一樣?會不會怪怪的?以 C 語言的角度來說,執行檔需要有個
main() (精確一點是 linker 設定的 entry function,在 gcc 中可以透過 -Wl,-eentry
輕易把他換掉的),而 library 就算有這個東西,也會是像去支援一般,並不會搶原本執行檔的起始點。
在 python 中,並沒有像 main() 這樣的 magic function,但假設我們一樣把這樣的內容放在
main() 中,慣例是使用 __name__ 來讓這件事做出區別,做出 script 的 entry point。
def main():
...
if __name__ == '__main__':
main()
如果某個 .py 檔被當成 script 時,__name__ 的值會是 '__main__' 這個字串。
因此檢查這個變數就可以確定現在的情境是跟 python interpreter 直接互動,
還是被當成一個 module import 進來的了。
這個 __name__ 代表的正是這個 module 的名稱,而 '__main__' 是一個特例,
代表這是一個 top-level module, 或被稱之為 __main__ module。
如果在 python 執行時,後面不加上任何的 .py 檔,還是會有一個的 built-in 的
__main__ module 產生。透過以下 command 可印印看它是什麼:
python -c 'import sys; print(sys.modules["__main__"])'
另一種從 command line 執行的方法為 python -m mod (PEP-338)。
這時參數中的 mod 代表的就是 module
名稱了。如果在執行的目錄下有一個 mod.py 檔案,就會直接去執行它。
這樣好像跟 python mod.py 沒有什麼不同之處。執行時一樣 __name__ 會是 '__main__',
可以用它來判斷是否要進入 main()。
重新整理一下在 command line 的幾種跑法:
python mod.py- run the scriptmod.pypython -m mod- run the modulemodpython -c 'import mod'- import the modulemod
1, 2 兩種,在 mod.py 中印出 __name__ 都會是 '__main__' 表示他是 top-level。
而 3,則會是 'mod',此時的 top-level 是 built-in 的 __main__ module。
那 1, 2 間有什麼不同呢?從參數的說明上就可以看出些端倪,
-m mod : run library module as a script (terminates option list)
滿大的差別是在 -m 對於 package 及 module 都有意義,並非只針對一個 .py 檔案。
如執行 python -m pkg 則 pkg 這目錄中必須有 __main__.py:
pkg/
+-- __init__.py
+-- __main__.py
在執行 python -m pkg 時,會發生一些滿不一樣的事,不再需要判斷 __name__
的機制,來檢驗是否是 top-level,直接把 main function 想要跑的東西寫在 __main__.py
當中就可以了。事實上,想要在 __init__.py 中用 __name__ 來判斷也不行,
因為在 __init__.py 中,__name__ 都會是 pkg,
直到 __main__.py 當中才會變成 '__main__'。
另一個 python -m 的不同就是它不只可以執行當下目錄的 module,只要是 sys.path 找得到的,
都可以透過這樣的方式來執行,有時 package 中有些可以執行的東西是也滿方便的。
比方說:
echo '{"x":1}' | python -m json.tool
另一種常見的用法是當 module 被執行時, 去跑它的 doctest, 讓 code、doc、test 三位一體。
if __name__ == "__main__":
import doctest
doctest.testmod()
p.s. 其他 __main__ module 跟 import 有關的東西,
直接看說明。
sys.path#
而為什麼 python -m mod 會找到 current working directory 下的 mod.py 呢?
因為在 python interpreter 執行時,會把 script 所在的目錄或當下的目錄加在 sys.path
的最前面,因此執行時目錄下的 mod.py 將第一個被找到。
sys.path 的決定及搜尋,大致描述在
sys.path
及 python3 - module search path
當中:
在 python 執行時
若參數中有帶
script.py,則會將 script 所在的目錄加到 sys.path 的第一個。如果 script 是一個 symbolic link,則目錄的計算會是以真實檔案所在的地方, 而非 symbolic 所在的位置。
這應該符合想像吧?不然就要把全部相關的 module 都建 symbolic 出去才能使用, 就失去 symbolic link 的意義了。
如果 python 執行時並沒有指定 script (包括 python -m 也一樣), 那會將 current working directory 加入 sys.path 的第一個。
這其實也是 script v.s. module 中很大的一個差別。
PYTHONPATH這個環境變數的內容,將會以:(os.path.pathsep) 切開後加到sys.path當中。python install 時的一些設定,及 site。這邊就很深入 python 啟動模式了, 不確定這個跟 site 與 install 設定的重疊部分有多少,或是執行過程是否有哪個包含哪個的關係。
只知道如 site 的說明文件所述,一些用來決定 path 的 module 不論是否有經過 site 的 configuration 來決定 sys.path,會被先 load 進來。
有關 site 的事,哪天要研究 venv 再來研究了。
sys.path 在程式初決定後,可以在任何執行的期間被改變,程式可以自己修正成想要的樣子。
在尋找 package/module 時,就是依照著他們的名稱,去找找看這些 sys.path 之下有沒有 match,有的話,就進入 load 階段。
sys.path 決定了第一層 package/module 的尋找過程,而 import 了第一個 module 後,
會有兩個方向的延伸:
import pkg.mod在 packagepkg當中尋找mod(把 package 當成 container)。 以 regular package 來說,也就是pkg所在的 folder 中去尋找mod.py。 但廣義的來說,是去pkg.__path__下面找mod.py,也就是在找mod.py時, 由pkg.__path__取代了sys.path的地位。在 module
modA.py當中import modB。即為需要考慮 relative import 的情況。
__path__ 與 relative import 在後面都會介紹得更詳細。
__name__ and sys.modules#
除了上面用檢查 __name__ 來判斷是否是 top-level 外,__name__ 在 module
的架構中有著非常重要的地位。
module 的 __name__ 是 module 存在 sys.modules 這個 dict 中的 key。
由 import statement 或 python command line 決定來決定。
整個 import 機制中,很多地方跟 __name__ 有關, 很重要的,不要亂改。
也要努力在 project 中確保它的唯一性,不然會很混亂。
__name__ 在 import document
中的說明,是一個 full qualified name。
也就是像 a.b.c 這樣的東西,描述了 module 的架構中的階層關係。
特別注意這邊階層關係指的是 module 架構,而非一般在存取它所用的 symbol table。比方說:
import sys
import pkg.mod as mod
print([x for x in sys.modules if not x.startswith('_')])
print([x for x in locals() if not x.startswith('_')])
在 import 時的 pkg.mod 是一個 full qualified name,代表會 load package pkg
到 sys.modules['pkg'] 接著 load pkg.mod 到 sys.modules['pkg.mod'],
最後設定 sys.modules['pkg'].mod = sys.modules['pkg.mod'] 建立階層關係。
而 as 後面的 mod 則代表這行 statement 所在的 module 的 symbol table 中要叫什麼名。
locals() or globals() 在 module level 會是一樣的東西,但如果在 function 的 scope
中,會塞到 locals() 當中。因此整個 statement 執行結束後,locals()['mod'] is mod
在這例子比較好解釋這兩個的不同,再來看一個比較混淆的例子:
import pkg.mod
與上個例子一樣,pkg.mod 在 sys.modules 中關係不變,但在 module symbol table 中,此時跟
mod 一點關係都沒有,只會有一個 pkg,並 reference 到 sys.modules['pkg']。
到此,對於 import 的機制多一層想像,import 不只從 file system 中把 module 讀進來,還在 process
中建立了 sys.modules 的 cache 及一個階層的世界,每個 __name__ 都像是個絕對路徑一樣。
binding 進 symbol table 只是其中一步,依照不同的 import 語法,將一個 sys.modules 中的
module binding 進去。以上面兩個例子來說就分別是 locals()['mod'] = sys.modules['pkg.mod'] 與
locals()['pkg'] = sys.modules['pkg']。
sys.modules 的設計,讓 python import module 時,同樣名稱的 module 不需要 load 兩次。
碰到之前已經存在的 module,就只需要從 sys.modules 中讀出 reference,
把後半的 symbol table binding 做完就好了。
因此,如果在一個大 project 當中,多次的 import 基本上都會指向同一個 module 物件
(如果不亂搞)。
__name__ 就像是一個絕對路徑,用這個來當成 cache 的 key。其決定方式,除了上面提到的
top-level module 一定會是 '__main__' 外,會是由 import 怎麼下相關的絕對路徑。
import pkg.mod 不只說明了要去找 pkg.mod,也暗示了這個 module 的 name 就是 pkg.mod。
或是 python -m pkg.mod 會是一樣的效果。都告訴 python 要找名為 pkg.mod 的 module。
但不幸的這個雖然說是絕對路徑,但是還是一個相對於 sys.path 的相對路徑。
如果試著將 pkg 加到 sys.path 當中,讓東西變得混亂一點來研究一下:
import sys
sys.path.insert(0, 'pkg')
import mod
import pkg.mod
這樣就會產生一個 sys.modules['mod'] 與 sys.modules['pkg.mod'],讓這個 module load
兩次了。
這個奇怪的例子告訴我們幾件事。
sys.path 不要這樣設,其中一個如果是另一個的子目錄,那就會發生這種事。 除非有特別理由,不然不要這樣吧... 同一個
mod.py,會被當成兩個 module。 感覺起來有點錯亂,pylint 也會因為這樣不太正常 (就是提醒你不要這樣寫的意思)。sys.path 在執行期不要變來變去,到底哪個是哪個會變得很難懂。要修正,盡量越早越好, 不然後面的命名會亂的。
能用 absolute import 就用,這樣
__name__就會非常明確,一目瞭然。 module 在 process 中的結構,就會像是 sys.path 中全部黏起來一樣,如果這些目錄第一層都不重名, 就存在非常強的 one-to-one mapping 關係。
雖說 absolute import 比較建議,但如想將 package 中的東西藏起來,切斷與外面的所有相依性時, relative import 變得十分吸引人。不會因為外部路徑的改變,需要變動內部所有 import 的名稱。
relative import#
考慮目錄結構為:
pkg/
+-- __init__.py
+-- A.py
+-- B.py
此時 A.py 中要 import B 時,該寫 import pkg.B 還是 import B 呢?
就上面講的,不論寫怎樣,希望的 B.__name__ 要是 pkg.B 才好,不然就有可能跟其他的
package 內的東西衝突了。
如果是 python2 的話,import B 是會成功的。而會自動地發現 B.py 是跟 A.py 在同一層,
然後利用 A 的 name pkg.A 來當成 B 的相對路徑,因此決定 B 的 __name__ 為 pkg.B。
事實上,在 python2 當中,去找同層的 B.py 優先序比其他的地方還要高,因此在這種情況下,
__name__ 是不能由 import 的語句來直接決定的,還要看它的執行地方。
雖然有它的好處,但也還滿惱人的,因為可能太多人覺得困擾了, PEP-328 加了另一種 explicit relative import 的語法,讓 relative import 能表明清楚。 而在 python3 中,就不再支援這樣的 implicit relative import 了。
強烈建議在 python2 中,加入
from __future__ import absolute_import
來拿掉 implicit relative import。為了 py2, py3 的相容性,也比較不容易錯亂。 pylint 會回報所有的 implicit relative import。
而 python3 explicit relative import 的寫法,是在 A.py 當中:
from . import B
這個 . 表示要和 A.py 在同一層的地方來做後面 import B 的解讀。
比較明確定表達了要 relative import 的語義。而 B 的 __name__ 就可從 A.__name__ 計算出來。
等一下,top-level module 的 __name__ 會是 '__main__',所以不能用
explicit relative import 的語法?
嗯,是的,對了一半。如果以 python pkg/A.py 的方式來執行,會發生
import __main__
Traceback (most recent call last):
File "lib/B.py", line 2, in <module>
from . import B
SystemError: Parent module '' not loaded, cannot perform relative import
但如果以 python -m pkg.A 來執行,則會順利跑過。
-m 的情況下,__name__ 也是 '__main__' 啊,所以說,從 __name__ 來推導 relative
import 並不是正確的說法。PEP-366,為了解決 python -m 的問題,加入了另一個抽象層,
在 python2.6 之後,另一個變數 __package__ 取代了由 __name__ 去計算 relative 的角色。
但如果不特別去設定這個東西的話,基本上跟上面講的機制是一模一樣的。在 module init 時,會自動由
__name__ 給計算出 __package__,但在 python -m 時,則有特別的判斷,在 __name__
被 '__main__' 壓掉之前,將 __name__.rpartition('.')[0] 存到了 __package__ 當中。
因此 explicit relative import 在 python -m 時,是可以正常運作的。
至於另一半要怎麼辦就不多說了,請自行觀賞 stack overflow, 反正 Guido 覺得是 antipattern。
I'm -1 on this and on any other proposed twiddlings of the main machinery. The only use case seems to be running scripts that happen to be living inside a module's directory, which I've always seen as an antipattern. To make me change my mind you'd have to convince me that it isn't.
package or module?#
上面一直隱隱約約的,好像 package 不是一個目錄,module 不是一個檔案一樣。
終於到了要面對這個問題的時候了。對,其實 package 是一個 module (is-A)。
如果用 python 的 type 來看 package,type 就是 types.ModuleType,
(印印 __mro__,他還是一個 object 呢)
那到底 package 與 module 的差別是什麼? 其實就跟 container 與 component 的關係差不多,"container is a component contained component(s)"。 只要能夠在裡面,讓 python interpreter 搜尋到 module,就可以稱之為 package 了?
在 python3 - module 的最後,終於透露了一個變數叫做 __path__,它是一個 iterable,
掌管著從 package 找到 module 之路的關鍵。
如果照著原本簡單的想法來思考,一般的 package 如果是 pkg/__init__.py 在 package load
的過程當中,__path__ 會被自動的設定成為 ['pkg']。
看起來好像是個相對路徑,這是因為執行時把 '' 放進 sys.path 的關係。
如果存放 pkg/ 的路徑在 sys.path 的第 i 個,基本上就會設定為
__path__ = [os.path.join(sys.path[i], 'pkg')]。
如果 sys.path[i] 為相對路徑,就會保持相對路徑。
p.s. 看到這邊是不是也覺得很可怕呢?process 的 current working directory 會影響到 import,
從 sys.path 會一路影響下去。
接著,處理 subpackage 或 module 與第一層的機制差不多,就是把 sys.path 的角色換為上一層 package 的
pkg.__path__。
與 sys.path 相同的,__path__ 一樣可以被動態的換掉,因此
可否沒有
pkg/__init__.py然後有pkg.mod這樣的 module?可以的,只要 create 一個
pkg.py然後在裡面把__path__指到另外一個下面有 mod.py 的目錄就行了。可否連
pkg.py都沒有,然後把一個目錄偽裝成pkg?可以的,直接從空中抓一個 module 出來,然後填上
__path__,並塞進sys.modules, 把名字取好叫做pkg。import sys, types class pkg(types.ModuleType): __path__ = ['pkg'] def __init__(self, *args): super(pkg, self).__init__(*args) sys.modules[self.__name__] = self _ = pkg('pkg') import pkg.A拜託不要這麼寫 code,能寫出這樣的東西還會動,只表示這些機制你懂了。 但懂了就好,project 當中不需要這樣搞大家的...
如果真的有一些 dynamic 的需求,可以先考慮寫後面會提到的 path_hooks 或 finder。
Package is a Python module which can contain submodules or recursively, subpackages. Technically, a package is a Python module with an path attribute.
在 python3 中,還有另一種叫做 namespace package 的可以沒有 __init__.py,
PEP 420 -- Implicit Namespace Packages,
像是用一個類 package 的東西,把在 file system 中不同地方串起來。
而有 __init__.py 的叫做 regular package。
原則上,在搜尋的機制當中,會優先去尋找 pkg/__init__.py,找到第一個就不會再繼續搜尋下去了
(找不到也才會去考慮 pkg.py,還有 pkg/),
因此如果沒有特別的狀況,還是用 regular package 把 __init__.py 建立出來,讓 import 省點工。
也明確很多。
更進一步,package 不一定是要是一個 folder 這件事,還有更多的可能性,比方說可以是一個 zip 檔。 在 PEP-237 Import Modules from Zip Archives 中定義了一些 zip package 的細節。
而 module 則除了 .py 檔案外,也可能是 .pyc、.pyd、.dll、.so 等的形式。
所有上面提到處理 .py 的搜尋,也都會處理這些副檔名。
module finder#
整理一下 import module 搜尋的順序為:
cached (
sys.modules),之前 import 過的 (偷偷塞進去的、不小心塞進去的...)總之存在就不會再搜尋了,也會跳過後面的編譯、執行動作,直接塞到 symbol table 中就結束了。
sys.modules中的東西是可以刪掉的,但刪掉並不代表 module 不見了,因為可能有其他的 module 會抓著原本的 module 的 reference。但是下次 import 這個 module 時就不會走 cache 了。如果要更新這個 module 的全部 reference,比方說某個 module 的 code 被改動了,想要換掉, 應該用
importlib.reload來做到。(reload package 不代表下面的 module 也會被換掉)定義在
sys.meta_path中的 findersdefault 有以下三個 finder,可註冊更多。只要 implement
find_spec(或在 py3.4 之前用find_module) 想要透過網路的方式去找 module 也是可以的。locate built-in modules
沒特別看到說明,但我猜是那些寫在 CPython 的 C code 內,那些也沒有
.py的東西。locate frozen modules
跟一些把 PVM 整包帶走的 freeze 機制有關, 一樣先跳過不研究。
path based finder - 在所有
sys.path(or__path__) 當中- 找到
pkg/__init__.py - 找到
pkg.py 若有出現名叫
pkg的目錄,會記錄起來以上
.py也可能是.pyc、.so等。而如果有 support zip package, 也會在這邊解開目錄結構去裡面搜尋可能的 module。除了一般的 file system path 之外,也可以透過 implement
sys.path_hooks處理一些 url、database。由於這個動作一般會 access disk 等較慢的 IO,因此會用到
sys.path_importer_cache來儲存 stat 或 listdir 的結果,加速往後的搜尋。可以手動清掉來強迫更新 cache。
- 找到
如果在 2 中沒有找到東西,但 2.3.3. 有記錄到一些同名的目錄,會建立
pkg的 namespace package 讓之後可以在下面嘗試找 module (python3+ implicit namespace package)
搜尋結束,如果找不到 module,raise ModuleNotFoundError。 如果找到 module,會回傳 module 的 spec,之後會用這個 spec 來 load module。 (py3.4+ spec 會帶一個 loader 一起 return 回來處理 loading 動作。)
在找這些名稱時,如果 python 認為這個 file system 是 case-insensitive 的,則會用 case-insensitive 的形式來進行 import PEP-235。 然而在 linux 上,應該會區分大小寫的,如果沒有分,請檢查 file system 或是 mount type, 搞不好掛了個遠端目錄。 因此在 windows 上寫 code 請小心,以免打錯字,測起來都是對的,到了 linux 上 import 失敗。 或是應該依據 gootle python naming rule, 全部都用小寫來命名 module name。
module loading & execution#
可以想像這個階段,大概有幾件事要處理:
create 一個 ModuleType 的物件,並 initialize 一些基本的變數。
需要 initialize 的變數,就如同前面講的,package 是要用來搜尋 module 的, 因此最少要把搜尋的這些東西先準備好 (
__name__、__path__等), 不然執行下去__init__.py之類的,可能會碰到要用的時候。將 module 塞到
sys.modules中。如果原本已經有 module 在
sys.modules當中,其實 1. 2. 不會跑,會直接用現存在sys.modules[spec.name]的 instance 繼續處理 loading,cache 的 lookup 及 shortcut 是再更前面的事,到這邊又存在 module instance 的話,是表示要做 reload。 改掉sys.modules的話,importlib.reload就無法達成修改在同一個 instance 上了。先塞入
sys.modules是 python 處理 cirular import 的關鍵。 雖然目前 module 中可能還沒有東西。但如果在整個 recursion import 的過程當中, 再次出現了同樣名稱的 module,就不會再次觸發 import 的這些大小事,直接返回 module 的 reference,繞過了這個階段的矛盾。但這樣的缺點是,
sys.modules中,有可能存在一些中間產物。 雖在失敗時會嘗試著抓到 exception 時把它移掉,但就算如此,其他 side-effect 的發生也不會回來了。 比方說 import 過程中如果還有 import 其他 module,這些 module 如果沒有發生問題,就還是都會在sys.modules當中了。執行該 script 的內容,將他們一一的塞到 module 當中
return sys.modules[spec.name]對的,是
sys.modules[spec.name]所以跟 1. 準備好的 module reference 可能又不同了, 雖然這樣很詭異,但 3. 可能跑一跑就把它換掉了...這邊維持著 python 一貫的精神,一切都是開放的,你要改就改,如果你覺得這樣是對的。 但我想這樣改應該會讓滿多 programmer 很意外的。
在這些階段當中,如果有錯誤發生,會 raise ImportError,除了 3. 中,execution
丟出什麼 exception 的話,將 sys.modules 的髒東西清掉後,就會直接再拋出去。
python3 - importlib 的說明當中,更加詳細的解釋了所有 interface, 所有相關的 PEP 也都列在這邊了。
circular import#
考慮 pkg/A.py 與 pkg/B.py 兩個在 pkg 中的 module。在 pkg/A.py 中
import pkg.B
def foo():
print('foo')
pkg/B.py 為
import pkg.A
def bar():
pkg.A.foo()
以上 code 透過執行 python -c 'import pkg.A' 來啟動。
由上面的 import 流程,可以知道 import 的部分會相安無事的成功。
sys.modules['pkg']被建立好,執行pkg/__init__.py,成功建立pkg,尋找A.pysys.modules['pkg.A']被建立出來,還是空的,此時去執行pkg/A.py- 在執行時,發現要
import pkg.Bsys.modules['pkg']存在,搞定- 在
sys.modules['pkg'].__path__中尋找B.py,不成問題 - 建立
sys.modules['pkg.B'],塞空 module,開始跑pkg/B.py- 嘗試
import pkg.A沒問題,中 cache - bind 到 local symbol table
locals()['pkg'] = sys.modules['pkg'] - 編譯
barfunction,存進bar.__code__裡,裡面跑啥現在根本不管。
- 嘗試
- B module 成功被 create
sys.modules['pkg'].B = sys.modules['pkg.B']
- bind 到 local symbol table
locals()['pkg'] = sys.modules['pkg'] - 編譯
foo
- 在執行時,發現要
- A module 成功 create
sys.modules['pkg'].A = sys.modules['pkg.A']
雖然成功了,但只要多加一行 _ = pkg.A 在 pkg/B.py 中,去存取它就會噴 exception 了。
因為 pkg is sys.modules['pkg'] 而 sys.modules['pkg'].A 還沒有值。
如果修改 pkg/B.py 為
import pkg.A as A
則會出現
AttributeError: 'module' object has no attribute 'A'
猜起來,應該是在 2.1.3.2. 的 bind 動作時,會改成 locals()['A'] = sys.modules['pkg'].A,
而這個動作是在 5. 中才執行,因此 sys.modules['pkg'] 中還沒有這個東西。
而如果 pkg/B.py 是這樣呢?
from pkg import A
# or
from . import A
這兩個很奇妙,在 python2 中,還是會失敗。但是 python3 就會成功了。
應該跟 python3 在 loader 那邊的改寫有關。失敗的 exception 都一樣,而成功,猜起來是變成
locals()['A'] = sys.modules['pkg.A'] 了。
照著這些 document 來看,失敗的原因都是 AttributeError,因此不會是 finder or loader。 應該都是 refernce 的方法或是 先後順序的問題。
在 python2 當中,如果使用 implicit import,就會改去抓 sys.modules['pkg.A'] 而成功了。
import A
而再把 pkg/B.py 改成這樣呢?
from .A import foo
def bar():
foo()
此時不論 python2 或 python3 又都會失敗了。想想如果 pkg/A.py 沒有變,那跑到這邊時,
不論由 from .X import... 是由 sys.modules['pkg'].A 還是 sys.modules['pkg.A']
去取得 A,其中都不可能有 foo,那行還沒有跑到啊。因此注定也是要失敗的。
但如果去修改 pkg/A.py 把 import 移到 foo 之後,想起來應該要成功了?
def foo():
print('foo')
import pkg.B
的確,python2、python3 現在一致通過了。是不是有一種在 javascript 當中用 == 的感覺啊...
所以與其了解這些會不會過,不如我們來好好處理 circular import 的問題,
沒有 circular 不就完全不會有這些煩惱了嗎?
如果在 pkg/A.py 與上面的一樣,pkg/__init__.py 中是這樣:
import pkg.A
foo = pkg.A.foo
這其實也會發生 circular import 了。
如果透過 python -c 'import pkg.A',嘗試查找 pkg 應該會發生多次,前幾次這個 module
都還是個半成品,但因執行到 foo = ... 時,pkg.A 已經準備完成,因此這段 code 跑起來非常正常。
很多 python code 其實都會發生這樣的事,比方說 importlib。
在處理 package 時,如果不允許 circular import,package 能做的事就廢掉一大半了,
當 code 多了也不能把它們從 __init__.py 當中搬到目錄下的其他 .py 中,很難做事。
python 已經是一個不用講明型態的語言了。
def login(user):
name = user.get_name()
...
根本不會知道 user 是一個什麼樣的 type,看到這邊只知道有 get_name。
如果只有這樣的 code,連 import 都不會有的,自然就不會有 circular import。
在 c/c++ 當中,因為得說明白 user 的 type,因此最少要 include header。
所以在 header 中也可能發生一些循環,而會用 header guard 去停止這種 recursion。
在 python 中,哪樣的東西會需要 import,又容易踩到 circular 的問題呢?
class inherit#
import pkg.A
class B(pkg.A.A):
...
繼承關係需要 import,而 pkg.A.A 的 dereference 會發生在 module 一行行執行時。 因此如果有這樣的關係,發生 circular 就很糟糕。
當有繼承關係,又有 abstract base class 的話,class member 被迫要拉到 class dict 當中, 此時可能又更加深 dependency。
class member (descriptor)#
import pkg.A
class B(object):
x = pkg.A.Property()
其他種類的 member 還可以考慮移到 __init__() 當中處理,但 descriptor 沒有辦法。
所以在 module import 時一定得對 pkg.A 做 dereference。
decorator#
import pkg.A
@pkg.A.wrap
def foo():
...
出現 circular 容易出事。class decorator 也都一樣。
in a function#
import pkg.A
def foo():
pkg.A.foo()
在 module import 時,這些都不用管了,只要 import 那行用 absolute import, 不要用 from,不要用 as,剩下都等執行期,不會在 import 時出事。
不過雖然沒有立即的危險,整個 project 還是會因為這樣 dependency 變得很亂。 賊船大概都是這樣上的。
detect circular import#
網路上有個 pycycle 的 project,會嘗試把 code
爬完來抓 cycle,但... 很不幸的,他不會處理 from ... import ...
(issue),
因此很多東西完全抓不出來。
pylint 也會幫忙抓,包括 import-self、cyclic-import。但也很不幸的,
看起來只要兩個 file 不是用同一個 pylint 跑的 (包括 -j) 就不會抓 cycle 了,
而全部都要跑完實在是會跑很久。
透過了解 import,利用 astroid 寫了一個 script。爬起來比 pylint 快一點, 順便建立 .dot 的 graph 以便後續分析。
https://github.com/sliverleaf/devtools/blob/master/bin/pydependency.py
留言