Python入門基礎(chǔ)之import機(jī)制
一、前言
本文基于開源項(xiàng)目:
補(bǔ)充擴(kuò)展講解,希望能夠讓讀者一文搞懂 Python 的 import 機(jī)制。
1.1 什么是 import 機(jī)制?
通常來(lái)講,在一段 Python 代碼中去執(zhí)行引用另一個(gè)模塊中的代碼,就需要使用 Python 的 import 機(jī)制。import 語(yǔ)句是觸發(fā) import 機(jī)制最常用的手段,但并不是唯一手段。
importlib.import_module 和 __import__ 函數(shù)也可以用來(lái)引入其他模塊的代碼。
1.2 import 是如何執(zhí)行的?
import 語(yǔ)句會(huì)執(zhí)行兩步操作:
- 搜索需要引入的模塊
- 將模塊的名字做為變量綁定到局部變量中
搜索步驟實(shí)際上是通過(guò) __import__ 函數(shù)完成的,而其返回值則會(huì)作為變量被綁定到局部變量中。下面我們會(huì)詳細(xì)聊到 __import__ 函數(shù)是如果運(yùn)作的。
二、import 機(jī)制概覽
下圖是 import 機(jī)制的概覽圖。不難看出,當(dāng) import 機(jī)制被觸發(fā)時(shí),Python 首先會(huì)去 sys.modules 中查找該模塊是否已經(jīng)被引入過(guò),如果該模塊已經(jīng)被引入了,就直接調(diào)用它,否則再進(jìn)行下一步。這里 sys.modules 可以看做是一個(gè)緩存容器。值得注意的是,如果 sys.modules 中對(duì)應(yīng)的值是 None 那么就會(huì)拋出一個(gè) ModuleNotFoundError 異常。下面是一個(gè)簡(jiǎn)單的實(shí)驗(yàn):
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 找到了對(duì)應(yīng)的 module,并且這個(gè) import 是由 import 語(yǔ)句觸發(fā)的,那么下一步將對(duì)把對(duì)應(yīng)的變量綁定到局部變量中。
如果沒(méi)有發(fā)現(xiàn)任何緩存,那么系統(tǒng)將進(jìn)行一個(gè)全新的 import 過(guò)程。在這個(gè)過(guò)程中 Python 將遍歷 sys.meta_path 來(lái)尋找是否有符合條件的元路徑查找器(meta path finder)。sys.meta_path 是一個(gè)存放元路徑查找器的列表。它有三個(gè)默認(rèn)的查找器:
- 內(nèi)置模塊查找器
- 凍結(jié)模塊(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 方法決定了該查找器是否能處理要引入的模塊并返回一個(gè) ModeuleSpec 對(duì)象,這個(gè)對(duì)象包含了用來(lái)加載這個(gè)模塊的相關(guān)信息。如果沒(méi)有合適的 ModuleSpec 對(duì)象返回,那么系統(tǒng)將查看 sys.meta_path 的下一個(gè)元路徑查找器。如果遍歷 sys.meta_path 都沒(méi)有找到合適的元路徑查找器,將拋出 ModuleNotFoundError。引入一個(gè)不存在的模塊就會(huì)發(fā)生這種情況,因?yàn)?sys.meta_path 中所有的查找器都無(wú)法處理這種情況:
In [1]: import nosuchmodule --------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) <ipython-input-1-40c387f4d718> in <module> ----> 1 import nosuchmodule ModuleNotFoundError: No module named 'nosuchmodule'
但是,如果這個(gè)手動(dòng)添加一個(gè)可以處理這個(gè)模塊的查找器,那么它也是可以被引入的:
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
可以看到,當(dāng)我們告訴系統(tǒng)如何去 find_spec 的時(shí)候,是不會(huì)拋出 ModuleNotFound 異常的。但是要成功加載一個(gè)模塊,還需要加載器 loader。
加載器是 ModuleSpec 對(duì)象的一個(gè)屬性,它決定了如何加載和執(zhí)行一個(gè)模塊。如果說(shuō) ModuleSpec 對(duì)象是“師父領(lǐng)進(jìn)門”的話,那么加載器就是“修行在個(gè)人”了。在加載器中,你完全可以決定如何來(lái)加載以及執(zhí)行一個(gè)模塊。這里的決定,不僅僅是加載和執(zhí)行模塊本身,你甚至可以修改一個(gè)模塊:
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
從上面的例子可以看到,一個(gè)加載器通常有兩個(gè)重要的方法 create_module 和 exec_module 需要實(shí)現(xiàn)。如果實(shí)現(xiàn)了 exec_module 方法,那么 create_module 則是必須的。如果這個(gè) import 機(jī)制是由 import 語(yǔ)句發(fā)起的,那么 create_module 方法返回的模塊對(duì)象對(duì)應(yīng)的變量將會(huì)被綁定到當(dāng)前的局部變量中。如果一個(gè)模塊因此成功被加載了,那么它將被緩存到 sys.modules。如果這個(gè)模塊再次被加載,那么 sys.modules 的緩存將會(huì)被直接引用。

三、import 勾子(import hooks)
為了簡(jiǎn)化,我們?cè)谏鲜龅牧鞒虉D中,并沒(méi)有提到 import 機(jī)制的勾子。實(shí)際上你可以添加一個(gè)勾子來(lái)改變 sys.meta_path 或者 sys.path,從而來(lái)改變 import 機(jī)制的行為。上面的例子中,我們直接修改了 sys.meta_path。實(shí)際上,你也可以通過(guò)勾子來(lái)實(shí)現(xiàn):
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 遍歷(當(dāng)然它們也可以通過(guò) import 勾子返回,參見上面的例子)。每個(gè)查找器必須實(shí)現(xiàn) find_spec 方法。如果一個(gè)查找器知道怎么處理將引入的模塊,find_spec 將返回一個(gè) ModuleSpec 對(duì)象(參見下節(jié))否則返回 None。
和之前提到的一樣 sys.meta_path 包含三種查找器:
- 內(nèi)置模塊查找器
- 凍結(jié)模塊查找器
- 基于路徑的查找器
這里我們想重點(diǎn)聊一聊基于路徑的查找器(path based finder)。它用于搜索一系列 import 路徑,每個(gè)路徑都用來(lái)查找是否有對(duì)應(yīng)的模塊可以加載。默認(rèn)的路徑查找器實(shí)現(xiàn)了所有在文件系統(tǒng)的特殊文件中查找模塊的功能,這些特殊文件包括 Python 源文件(.py 文件),Python 編譯后代碼文件(.pyc 文件),共享庫(kù)文件(.so 文件)。如果 Python 標(biāo)準(zhǔn)庫(kù)中包含 zipimport,那么相關(guān)的文件也可用來(lái)查找可引入的模塊。
路徑查找器不僅限于文件系統(tǒng)中的文件,它還可以上 URL 數(shù)據(jù)庫(kù)的查詢,或者其他任何可以用字符串表示的地址。
你可以用上節(jié)提供的勾子來(lái)實(shí)現(xiàn)對(duì)同類型地址的模塊查找。例如,如果你想通過(guò) URL 來(lái) import 模塊,那么你可以寫一個(gè) import 勾子來(lái)解析這個(gè) URL 并且返回一個(gè)路徑查找器。
注意,路徑查找器不同于元路徑查找器。后者在 sys.meta_path 中用于被 Python 遍歷,而前者特指基于路徑的查找器。
五、ModuleSpec 對(duì)象
每個(gè)元路徑查找器必須實(shí)現(xiàn) find_spec 方法,如果該查找器知道如果處理要引入的模塊,那么這個(gè)方法將返回一個(gè) ModuleSpec 對(duì)象。這個(gè)對(duì)象有兩個(gè)屬性值得一提,一個(gè)是模塊的名字,而另一個(gè)則是查找器。如果一個(gè) ModuleSpec 對(duì)象的查找器是 None,那么類似 ImportError: missing loader 的異常將會(huì)被拋出。查找器將用來(lái)創(chuàng)建和執(zhí)行一個(gè)模塊(見下節(jié))。
你可以通過(guò) <module>.__spec__ 來(lái)查找模塊的 ModuleSpec 對(duì)象:
In [1]: import sys In [2]: sys.__spec__ Out[2]: ModuleSpec(name='sys', loader=<class '_frozen_importlib.BuiltinImporter'>)
六、加載器(loader)
加載器通過(guò) create_module 來(lái)創(chuàng)建模塊以及 exec_module 來(lái)執(zhí)行模塊。通常如果一個(gè)模塊是一個(gè) Python 模塊(非內(nèi)置模塊或者動(dòng)態(tài)擴(kuò)展),那么該模塊的代碼需要在模塊的 __dict__ 空間上執(zhí)行。如果模塊的代碼無(wú)法執(zhí)行,那么就會(huì)拋出 ImportError 異常,或者其他在執(zhí)行過(guò)程中的異常也會(huì)被拋出。
絕大多數(shù)情況下,查找器和加載器是同一個(gè)東西。這種情況下,查找器的 find_spec 方法返回的 ModuleSpec 對(duì)象的 loader 屬性將指向它自己。
我們可以用 create_module 來(lái)動(dòng)態(tài)創(chuàng)建一個(gè)模塊,如果它返回 None Python 會(huì)自動(dòng)創(chuàng)建一個(gè)模塊。
七、總結(jié)
Python 的 import 機(jī)制靈活而強(qiáng)大。以上的介紹大部分是基于官方文檔,以及較新的 Python 3.6+ 版本。由于篇幅,還有很多細(xì)節(jié)并沒(méi)有包含其中,例如子模塊的加載、模塊代碼的緩存機(jī)制等等。文章中也難免出現(xiàn)紕漏如果有任何問(wèn)題,歡迎到 github.com/pwwang/pyth… 開 issue 提問(wèn)及討論。
到此這篇關(guān)于Python入門基礎(chǔ)之import機(jī)制的文章就介紹到這了,更多相關(guān)Python import機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python?Decorator裝飾器的創(chuàng)建方法及常用場(chǎng)景分析
這篇文章主要介紹了Python?Decorator裝飾器的創(chuàng)建方法及常用場(chǎng)景,裝飾器可以分成方法裝飾器和類裝飾器,他們的區(qū)別是一個(gè)是用函數(shù)實(shí)現(xiàn)的裝飾器,一個(gè)是用類實(shí)現(xiàn)的裝飾器,他們也都能在方法和類上進(jìn)行裝飾,需要的朋友可以參考下2022-07-07
Matlab中的mat數(shù)據(jù)轉(zhuǎn)成python中使用的npy數(shù)據(jù)遇到的坑及解決
這篇文章主要介紹了Matlab中的mat數(shù)據(jù)轉(zhuǎn)成python中使用的npy數(shù)據(jù)遇到的坑及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
詳解常用查找數(shù)據(jù)結(jié)構(gòu)及算法(Python實(shí)現(xiàn))
本篇文章主要介紹了Python實(shí)現(xiàn)常用查找數(shù)據(jù)結(jié)構(gòu)及算法,具有一定的參考價(jià)值,有興趣的可以了解一下。2016-12-12
Python3讀寫Excel文件(使用xlrd,xlsxwriter,openpyxl3種方式讀寫實(shí)例與優(yōu)劣)
這篇文章主要介紹了Python3讀寫Excel文件,使用xlrd,xlsxwriter,openpyxl3種方式讀寫實(shí)例與優(yōu)劣,需要的朋友可以參考下2020-02-02
基于Python pyecharts實(shí)現(xiàn)多種圖例代碼解析
這篇文章主要介紹了基于Python pyecharts實(shí)現(xiàn)多種圖例代碼解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
django傳值給模板, 再用JS接收并進(jìn)行操作的實(shí)例
今天小編就為大家分享一篇django傳值給模板, 再用JS接收并進(jìn)行操作的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
python基礎(chǔ)學(xué)習(xí)之生成器與文件系統(tǒng)知識(shí)總結(jié)
本文是參考《python數(shù)據(jù)分析》的附錄對(duì)生成器和文件系統(tǒng)結(jié)合案例的一個(gè)簡(jiǎn)單回顧,文中對(duì)python生成器與文件系統(tǒng)作了非常詳細(xì)的介紹,對(duì)正在學(xué)習(xí)python的小伙伴們有很好地幫助,需要的朋友可以參考下2021-05-05

