跳到主要內容

Python - import

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 到 module mod。
import pkg.mod
找到 package pkg 後,找到 module mod,在 local symbol table 中建立 pkg。之後可以透過 pkg.mod.X 去調用下面的東西。
import pkg.mod as mod
匯入後改變在 local symbol table 中的名稱。在 pkg.mod import 後,可用 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 的流程會有

  1. 搜尋 (find) - 怎麼找到 pkg/,怎麼找到 pkg/mod.py
  2. 編譯、執行
  3. 將 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 的幾種跑法:

  1. python mod.py - run the script mod.py
  2. python -m mod - run the module mod
  3. python -c 'import mod' - import the module mod

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 當中:

  1. 在 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 中很大的一個差別。

  2. PYTHONPATH 這個環境變數的內容,將會以 : (os.path.pathsep) 切開後加到 sys.path 當中。

  3. 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 後, 會有兩個方向的延伸:

  1. import pkg.mod 在 package pkg 當中尋找 mod (把 package 當成 container)。 以 regular package 來說,也就是 pkg 所在的 folder 中去尋找 mod.py。 但廣義的來說,是去 pkg.__path__ 下面找 mod.py,也就是在找 mod.py 時, 由 pkg.__path__ 取代了 sys.path 的地位。

  2. 在 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 兩次了。

這個奇怪的例子告訴我們幾件事。

  1. sys.path 不要這樣設,其中一個如果是另一個的子目錄,那就會發生這種事。 除非有特別理由,不然不要這樣吧... 同一個 mod.py,會被當成兩個 module。 感覺起來有點錯亂,pylint 也會因為這樣不太正常 (就是提醒你不要這樣寫的意思)。

  2. sys.path 在執行期不要變來變去,到底哪個是哪個會變得很難懂。要修正,盡量越早越好, 不然後面的命名會亂的。

  3. 能用 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__ 一樣可以被動態的換掉,因此

  1. 可否沒有 pkg/__init__.py 然後有 pkg.mod 這樣的 module?

    可以的,只要 create 一個 pkg.py 然後在裡面把 __path__ 指到另外一個下面有 mod.py 的目錄就行了。

  2. 可否連 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 搜尋的順序為:

  1. cached (sys.modules),之前 import 過的 (偷偷塞進去的、不小心塞進去的...)

    總之存在就不會再搜尋了,也會跳過後面的編譯、執行動作,直接塞到 symbol table 中就結束了。 sys.modules 中的東西是可以刪掉的,但刪掉並不代表 module 不見了,因為可能有其他的 module 會抓著原本的 module 的 reference。但是下次 import 這個 module 時就不會走 cache 了。

    如果要更新這個 module 的全部 reference,比方說某個 module 的 code 被改動了,想要換掉, 應該用 importlib.reload 來做到。(reload package 不代表下面的 module 也會被換掉)

  2. 定義在 sys.meta_path 中的 finders

    default 有以下三個 finder,可註冊更多。只要 implement find_spec (或在 py3.4 之前用 find_module) 想要透過網路的方式去找 module 也是可以的。

    1. locate built-in modules

      沒特別看到說明,但我猜是那些寫在 CPython 的 C code 內,那些也沒有 .py 的東西。

    2. locate frozen modules

      跟一些把 PVM 整包帶走的 freeze 機制有關, 一樣先跳過不研究。

    3. path based finder - 在所有 sys.path (or __path__) 當中

      1. 找到 pkg/__init__.py
      2. 找到 pkg.py
      3. 若有出現名叫 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。

  3. 如果在 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#

可以想像這個階段,大概有幾件事要處理:

  1. create 一個 ModuleType 的物件,並 initialize 一些基本的變數。

    需要 initialize 的變數,就如同前面講的,package 是要用來搜尋 module 的, 因此最少要把搜尋的這些東西先準備好 (__name__、__path__ 等), 不然執行下去 __init__.py 之類的,可能會碰到要用的時候。

  2. 將 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 當中了。

  3. 執行該 script 的內容,將他們一一的塞到 module 當中

  4. 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 的部分會相安無事的成功。

  1. sys.modules['pkg'] 被建立好,執行 pkg/__init__.py,成功建立 pkg,尋找 A.py
  2. sys.modules['pkg.A'] 被建立出來,還是空的,此時去執行 pkg/A.py
    1. 在執行時,發現要 import pkg.B
      1. sys.modules['pkg'] 存在,搞定
      2. 在 sys.modules['pkg'].__path__ 中尋找 B.py,不成問題
      3. 建立 sys.modules['pkg.B'],塞空 module,開始跑 pkg/B.py
        1. 嘗試 import pkg.A 沒問題,中 cache
        2. bind 到 local symbol table locals()['pkg'] = sys.modules['pkg']
        3. 編譯 bar function,存進 bar.__code__ 裡,裡面跑啥現在根本不管。
      4. B module 成功被 create
      5. sys.modules['pkg'].B = sys.modules['pkg.B']
    2. bind 到 local symbol table locals()['pkg'] = sys.modules['pkg']
    3. 編譯 foo
  3. A module 成功 create
  4. 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

留言

這個網誌中的熱門文章

Resolve cycle

Resolve cycle # 之前用 astroid 寫的 tool pydependency , 將 dependency graph 匯出成 .dot 或是一些 .json 格式後, 就有幾個方便的 tool 可以來分析了。 以下的故事基本上都是在分析 junyi-academy 的 code,但大部分的壞味道,從 khan-academy 時代就有了。如果只看 dependency graph 沒感覺的話,也可以載 source code 來看看。 graph tools # graphviz # graphviz 為老牌的 graph 視覺化工具。 pylint 的內含的工具 pyreverse 就是拿 graphviz 來畫 UML 的圖的。好處就是簡單好上手,畫出第一張圖: echo "digraph { hello->world }" | dot -Tpng -o dot.png 強大之處在於其 layout 演算法,但是如果 cycle 很多,畫出來就會全部結成一團, 也不知道能怎麼調整。 python 部分套件可以安裝 pygraphviz 或 pydot,讀寫 dot 會方便些。 pygraphviz 包裝原生 graphviz library 的 C api 到 python 中,所以對於 agraph 的操作是直接對應到 C 的 data structure 的。 而 pydot 對於 graph 的操作都還是在 python 當中。直到要找 graphviz tool 時, 才利用其中的 parser 作資料的轉換。 兩個在 layout/draw 時,都還是直接用 subprocess.Popen 呼叫 graphviz 的 command line tools。pygraphviz 也沒有因為直接呼叫 C,而直接用像 gvToolTred 這樣的 function。還是多了一層 serialize、deserialize 及 pipe。 在 tred 或是 layout (只需要多標上 position?) 這樣的功能上,感覺滿慘的。 tred 是一個 grap...

Blogger tool

Blogger tool # 不懂為什麼,對於所見即所得的編輯器不太會用(少了 vim 加持??) 直接寫 html 又太痛苦。那要寫 markdown 嗎? 希望在文件中方便插入 code、流程圖等。 找其他 blog?就只是記記東西還要去找,有點懶。 如果透過 markdown tool,再貼到 blogger,html tag 很髒,有些東西也可能會跑掉。 目前 markdown 也不符合需求,需要小改。未來如果不滿意,還是可以隨時調整。 因此就決定自己 render 然後推上 blogger。 How to push posts # 1. 申請一個 app id/secret # 不知道有沒有別的申請方法,目前是直接登入 console.cloud.google.com 來申請。 然後隨便找一個 project (這樣對嗎?邏輯上這樣比較像是要把存取 blogger 的權限放到這個 web application 當中一樣。但其實暫時還只打算先跑 CLI)。 先把 blogger API 打開,然後到 "API 和服務" > "憑證" 去建立。 申請過程中會問一下是要幹什麼用的,還有填個 app 的名字, 然後就可以下載 client_id.json 裡面帶有 api id/secret 了。 如果弄丟了,或要修改,之後也可回到這個頁面當中做後續的維護動作。 2. install API client # 沒有其實也可以啦,手動打 REST API 而已。只是有好像比較方便一些。 python 版 pip install --upgrade google-api-python-client nodejs 版 (alpha) npm install googleapis --save 最後選用了 nodejs,因為 python-markdown 出來的結果不是很滿意 (另一個 misaka 沒試過) , marked 之前用起來感覺還不錯。只用同一種 language 之後在修改時應該比較方便擴充。 3. get access token # 因為需要操作 ...