本文基於開源項目:
https://github.com/pwwang/python-import-system
補充擴展講解,但願可以讓讀者一文搞懂 Python 的 import 機制。javascript
一般來說,在一段 Python 代碼中去執行引用另外一個模塊中的代碼,就須要使用 Python 的 import 機制。import 語句是觸發 import 機制最經常使用的手段,但並非惟一手段。java
importlib.import_module 和 __ import__ 函數也能夠用來引入其餘模塊的代碼。python
import 語句會執行兩步操做:
1.搜索須要引入的模塊
2.將模塊的名字作爲變量綁定到局部變量中git
搜索步驟其實是經過 __ import__ 函數完成的,而其返回值則會做爲變量被綁定到局部變量中。下面咱們會詳細聊到 __ import__ 函數是若是運做的。github
下圖是 import 機制的概覽圖。不難看出,當 import 機制被觸發時,Python 首先會去 sys.modules 中查找該模塊是否已經被引入過,若是該模塊已經被引入了,就直接調用它,不然再進行下一步。這裏 sys.modules 能夠看作是一個緩存容器。值得注意的是,若是 sys.modules 中對應的值是 None 那麼就會拋出一個 ModuleNotFoundError 異常。下面是一個簡單的實驗:數據庫
In [1]: import sys In [2]: sys.modules['os'] = None In [3]: import os --------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) <ipython-input-3-543d7f3a58ae> in <module> ----> 1 import os ModuleNotFoundError: import of os halted; None in sys.modules
若是在 sys.modules 找到了對應的 module,而且這個 import 是由 import 語句觸發的,那麼下一步將對把對應的變量綁定到局部變量中。緩存
若是沒有發現任何緩存,那麼系統將進行一個全新的 import 過程。在這個過程當中 Python 將遍歷 sys.meta_path 來尋找是否有符合條件的元路徑查找器(meta path finder)。sys.meta_path 是一個存放元路徑查找器的列表。它有三個默認的查找器:ruby
In [1]: import sys In [2]: sys.meta_path Out[2]: [_frozen_importlib.BuiltinImporter, _frozen_importlib.FrozenImporter, _frozen_importlib_external.PathFinder]
查找器的 find_spec 方法決定了該查找器是否能處理要引入的模塊並返回一個 ModeuleSpec 對象,這個對象包含了用來加載這個模塊的相關信息。若是沒有合適的 ModuleSpec 對象返回,那麼系統將查看 sys.meta_path 的下一個元路徑查找器。若是遍歷 sys.meta_path 都沒有找到合適的元路徑查找器,將拋出 ModuleNotFoundError。引入一個不存在的模塊就會發生這種狀況,由於 sys.meta_path 中全部的查找器都沒法處理這種狀況:函數
In [1]: import nosuchmodule --------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) <ipython-input-1-40c387f4d718> in <module> ----> 1 import nosuchmodule ModuleNotFoundError: No module named 'nosuchmodule'
可是,若是這個手動添加一個能夠處理這個模塊的查找器,那麼它也是能夠被引入的:ui
In [1]: import sys ...: ...: from importlib.abc import MetaPathFinder ...: from importlib.machinery import ModuleSpec ...: ...: class NoSuchModuleFinder(MetaPathFinder): ...: def find_spec(self, fullname, path, target=None): ...: return ModuleSpec('nosuchmodule', None) ...: ...: # don't do this in your script ...: sys.meta_path = [NoSuchModuleFinder()] ...: ...: import nosuchmodule --------------------------------------------------------------------------- ImportError Traceback (most recent call last) <ipython-input-6-b7cbf7e60adc> in <module> 11 sys.meta_path = [NoSuchModuleFinder()] 12 ---> 13 import nosuchmodule ImportError: missing loader
能夠看到,當咱們告訴系統如何去 find_spec 的時候,是不會拋出 ModuleNotFound 異常的。可是要成功加載一個模塊,還須要加載器 loader。
加載器是 ModuleSpec 對象的一個屬性,它決定了如何加載和執行一個模塊。若是說 ModuleSpec 對象是「師父領進門」的話,那麼加載器就是「修行在我的」了。在加載器中,你徹底能夠決定如何來加載以及執行一個模塊。這裏的決定,不只僅是加載和執行模塊自己,你甚至能夠修改一個模塊:
In [1]: import sys ...: from types import ModuleType ...: from importlib.machinery import ModuleSpec ...: from importlib.abc import MetaPathFinder, Loader ...: ...: class Module(ModuleType): ...: def __init__(self, name): ...: self.x = 1 ...: self.name = name ...: ...: class ExampleLoader(Loader): ...: def create_module(self, spec): ...: return Module(spec.name) ...: ...: def exec_module(self, module): ...: module.y = 2 ...: ...: class ExampleFinder(MetaPathFinder): ...: def find_spec(self, fullname, path, target=None): ...: return ModuleSpec('module', ExampleLoader()) ...: ...: sys.meta_path = [ExampleFinder()] In [2]: import module In [3]: module Out[3]: <module 'module' (<__main__.ExampleLoader object at 0x7f7f0d07f890>)> In [4]: module.x Out[4]: 1 In [5]: module.y Out[5]: 2
從上面的例子能夠看到,一個加載器一般有兩個重要的方法 create_module 和 exec_module 須要實現。若是實現了 exec_module 方法,那麼 create_module 則是必須的。若是這個 import 機制是由 import 語句發起的,那麼 create_module 方法返回的模塊對象對應的變量將會被綁定到當前的局部變量中。若是一個模塊所以成功被加載了,那麼它將被緩存到 sys.modules。若是這個模塊再次被加載,那麼 sys.modules 的緩存將會被直接引用。
爲了簡化,咱們在上述的流程圖中,並無提到 import 機制的勾子。實際上你能夠添加一個勾子來改變 sys.meta_path 或者 sys.path,從而來改變 import 機制的行爲。上面的例子中,咱們直接修改了 sys.meta_path。實際上,你也能夠經過勾子來實現:
In [1]: import sys ...: from types import ModuleType ...: from importlib.machinery import ModuleSpec ...: from importlib.abc import MetaPathFinder, Loader ...: ...: class Module(ModuleType): ...: def __init__(self, name): ...: self.x = 1 ...: self.name = name ...: ...: class ExampleLoader(Loader): ...: def create_module(self, spec): ...: return Module(spec.name) ...: ...: def exec_module(self, module): ...: module.y = 2 ...: ...: class ExampleFinder(MetaPathFinder): ...: def find_spec(self, fullname, path, target=None): ...: return ModuleSpec('module', ExampleLoader()) ...: ...: def example_hook(path): ...: # some conditions here ...: return ExampleFinder() ...: ...: sys.path_hooks = [example_hook] ...: # force to use the hook ...: sys.path_importer_cache.clear() ...: ...: import module ...: module Out[1]: <module 'module' (<__main__.ExampleLoader object at 0x7fdb08f74b90>)>
元路徑查找器的工做就是看是否能找到模塊。這些查找器存放在 sys.meta_path 中以供 Python 遍歷(固然它們也能夠經過 import 勾子返回,參見上面的例子)。每一個查找器必須實現 find_spec 方法。若是一個查找器知道怎麼處理將引入的模塊,find_spec 將返回一個 ModuleSpec 對象(參見下節)不然返回 None。
和以前提到的同樣 sys.meta_path 包含三種查找器:
這裏咱們想重點聊一聊基於路徑的查找器(path based finder)。它用於搜索一系列 import 路徑,每一個路徑都用來查找是否有對應的模塊能夠加載。默認的路徑查找器實現了全部在文件系統的特殊文件中查找模塊的功能,這些特殊文件包括 Python 源文件(.py 文件),Python 編譯後代碼文件(.pyc 文件),共享庫文件(.so 文件)。若是 Python 標準庫中包含 zipimport,那麼相關的文件也可用來查找可引入的模塊。
路徑查找器不只限於文件系統中的文件,它還能夠上 URL 數據庫的查詢,或者其餘任何能夠用字符串表示的地址。
你能夠用上節提供的勾子來實現對同類型地址的模塊查找。例如,若是你想經過 URL 來 import 模塊,那麼你能夠寫一個 import 勾子來解析這個 URL 而且返回一個路徑查找器。
注意,路徑查找器不一樣於元路徑查找器。後者在 sys.meta_path 中用於被 Python 遍歷,而前者特指基於路徑的查找器。
每一個元路徑查找器必須實現 find_spec 方法,若是該查找器知道若是處理要引入的模塊,那麼這個方法將返回一個 ModuleSpec 對象。這個對象有兩個屬性值得一提,一個是模塊的名字,而另外一個則是查找器。若是一個 ModuleSpec 對象的查找器是 None,那麼相似 ImportError: missing loader 的異常將會被拋出。查找器將用來建立和執行一個模塊(見下節)。
你能夠經過 <module>.spec 來查找模塊的 ModuleSpec 對象:
In [1]: import sys In [2]: sys.__spec__ Out[2]: ModuleSpec(name='sys', loader=<class '_frozen_importlib.BuiltinImporter'>)
加載器經過 create_module 來建立模塊以及 exec_module 來執行模塊。一般若是一個模塊是一個 Python 模塊(非內置模塊或者動態擴展),那麼該模塊的代碼須要在模塊的 __ dict__ 空間上執行。若是模塊的代碼沒法執行,那麼就會拋出ImportError 異常,或者其餘在執行過程當中的異常也會被拋出。
絕大多數狀況下,查找器和加載器是同一個東西。這種狀況下,查找器的 find_spec 方法返回的 ModuleSpec 對象的 loader 屬性將指向它本身。
咱們能夠用 create_module 來動態建立一個模塊,若是它返回 None Python 會自動建立一個模塊。
Python 的 import 機制靈活而強大。以上的介紹大部分是基於官方文檔,因爲篇幅,還有不少細節並無包含其中,例如子模塊的加載、模塊代碼的緩存機制等等。