一、前言
本文基于開源專案:
https://github.com/pwwang/python-import-system
補充擴展講解,希望能夠讓讀者一文搞懂 Python 的 import 機制,
1.1 什么是 import 機制?
通常來講,在一段 Python 代碼中去執行參考另一個模塊中的代碼,就需要使用 Python 的 import 機制,import 陳述句是觸發 import 機制最常用的手段,但并不是唯一手段,
importlib.import_module 和 __ import__ 函式也可以用來引入其他模塊的代碼,
1.2 import 是如何執行的?
import 陳述句會執行兩步操作:
1.搜索需要引入的模塊
2.將模塊的名字做為變數系結到區域變數中
搜索步驟實際上是通過 __ import__ 函式完成的,而其回傳值則會作為變數被系結到區域變數中,下面我們會詳細聊到 __ import__ 函式是如果運作的,
二、import 機制概覽
下圖是 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 是一個存放元路徑查找器的串列,它有三個默認的查找器:
- 內置模塊查找器
- 凍結模塊(frozen module)查找器
- 基于路徑的模塊查找器,
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'
但是,如果這個手動添加一個可以處理這個模塊的查找器,那么它也是可以被引入的:
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 勾子(import hooks)
為了簡化,我們在上述的流程圖中,并沒有提到 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>)>
四、元路徑查找器(meta path finder)
元路徑查找器的作業就是看是否能找到模塊,這些查找器存放在 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 遍歷,而前者特指基于路徑的查找器,
五、ModuleSpec 物件
每個元路徑查找器必須實作 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'>)
六、加載器(loader)
加載器通過 create_module 來創建模塊以及 exec_module 來執行模塊,通常如果一個模塊是一個 Python 模塊(非內置模塊或者動態擴展),那么該模塊的代碼需要在模塊的 __ dict__ 空間上執行,如果模塊的代碼無法執行,那么就會拋出ImportError 例外,或者其他在執行程序中的例外也會被拋出,
絕大多數情況下,查找器和加載器是同一個東西,這種情況下,查找器的 find_spec 方法回傳的 ModuleSpec 物件的 loader 屬性將指向它自己,
我們可以用 create_module 來動態創建一個模塊,如果它回傳 None Python 會自動創建一個模塊,
七、總結
Python 的 import 機制靈活而強大,以上的介紹大部分是基于官方檔案,由于篇幅,還有很多細節并沒有包含其中,例如子模塊的加載、模塊代碼的快取機制等等,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/263658.html
標籤:Python
下一篇:Python教程:抽象類與歸一化
