本篇文章為大家展示了Python的import 機制中如何實現(xiàn)遠(yuǎn)程導(dǎo)入模塊,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
成都創(chuàng)新互聯(lián)長期為上千多家客戶提供的網(wǎng)站建設(shè)服務(wù),團隊從業(yè)經(jīng)驗10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為港閘企業(yè)提供專業(yè)的網(wǎng)站制作、成都網(wǎng)站設(shè)計,港閘網(wǎng)站改版等技術(shù)服務(wù)。擁有十多年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。
所謂的模塊導(dǎo)入,是指在一個模塊中使用另一個模塊的代碼的操作,它有利于代碼的復(fù)用。
也許你看到這個標(biāo)題,會說我怎么會發(fā)這么基礎(chǔ)的文章?(當(dāng)然也會有基礎(chǔ)的文章啦)
與此相反。恰恰我覺得這篇文章的內(nèi)容可以算是 Python 的進階技能,會深入地探討并以真實案例講解 Python import Hook 的知識點。
當(dāng)然為了使文章更系統(tǒng)、全面,前面會有小篇幅講解基礎(chǔ)知識點,但希望你能有耐心的往后讀下去,因為后面才是本篇文章的精華所在,希望你不要錯過。
1. 導(dǎo)入系統(tǒng)的基礎(chǔ)
1.1 導(dǎo)入單元構(gòu)成
導(dǎo)入單元有多種,可以是模塊、包及變量等。
對于這些基礎(chǔ)的概念,對于新手還是有必要介紹一下它們的區(qū)別。
模塊:類似 *.py,*.pyc, *.pyd ,*.so,*.dll 這樣的文件,是 Python 代碼載體的最小單元。
包 還可以細(xì)分為兩種:
Regular packages:是一個帶有 __init__.py 文件的文件夾,此文件夾下可包含其他子包,或者模塊
Namespace packages
關(guān)于 Namespace packages,有的人會比較陌生,我這里摘抄官方文檔的一段說明來解釋一下。
Namespace packages 是由多個 部分 構(gòu)成的,每個部分為父包增加一個子包。各個部分可能處于文件系統(tǒng)的不同位置。部分也可能處于 zip 文件中、網(wǎng)絡(luò)上,或者 Python 在導(dǎo)入期間可以搜索的其他地方。命名空間包并不一定會直接對應(yīng)到文件系統(tǒng)中的對象;它們有可能是無實體表示的虛擬模塊。
命名空間包的 __path__ 屬性不使用普通的列表。而是使用定制的可迭代類型,如果其父包的路徑 (或者最高層級包的 sys.path) 發(fā)生改變,這種對象會在該包內(nèi)的下一次導(dǎo)入嘗試時自動執(zhí)行新的對包部分的搜索。
命名空間包沒有 parent/__init__.py 文件。實際上,在導(dǎo)入搜索期間可能找到多個 parent 目錄,每個都由不同的部分所提供。因此 parent/one 的物理位置不一定與 parent/two 相鄰。在這種情況下,Python 將為頂級的 parent 包創(chuàng)建一個命名空間包,無論是它本身還是它的某個子包被導(dǎo)入。
1.2 相對/絕對對導(dǎo)入
當(dāng)我們 import 導(dǎo)入模塊或包時,Python 提供兩種導(dǎo)入方式:
相對導(dǎo)入(relative import ):import foo.bar 或者 form foo import bar
絕對導(dǎo)入(absolute import):from . import B 或 from ..A import B,其中.表示當(dāng)前模塊,..表示上層模塊
你可以根據(jù)實際需要進行選擇,但有必要說明的是,在早期的版本( Python2.6 之前),Python 默認(rèn)使用的相對導(dǎo)入。而后來的版本中( Python2.6 之后),都以絕對導(dǎo)入為默認(rèn)使用的導(dǎo)入方式。
使用絕對路徑和相對路徑各有利弊:
當(dāng)你在開發(fā)維護自己的項目時,應(yīng)當(dāng)使用相對路徑導(dǎo)入,這樣可以避免硬編碼帶來的麻煩。
而使用絕對路徑,會讓你模塊導(dǎo)入結(jié)構(gòu)更加清晰,而且也避免了重名的包沖突而導(dǎo)入錯誤。
1.3 導(dǎo)入的標(biāo)準(zhǔn)寫法
在 PEP8 中有一條,對模塊的導(dǎo)入順序提出了要求,不同來源模塊導(dǎo)入,應(yīng)該有清晰的界限,使用一空行來分開。
import 語句應(yīng)當(dāng)分行書寫
# bad import os,sys # good import os import sys
import語句應(yīng)當(dāng)使用absolute import
# bad from ..bar import Bar # good from foo.bar import test
import語句應(yīng)當(dāng)放在文件頭部,置于模塊說明及docstring之后,全局變量之前
import語句應(yīng)該按照順序排列,每組之間用一個空格分隔,按照內(nèi)置模塊,第三方模塊,自己所寫的模塊調(diào)用順序,同時每組內(nèi)部按照字母表順序排列
# 內(nèi)置模塊 import os import sys # 第三方模塊 import flask # 本地模塊 from foo import bar
我記得這塊兒在之前的Python學(xué)習(xí)教程中有跟大家講到過!
2. __import__ 的妙用
在 Python 中使用 import 關(guān)鍵字來實現(xiàn)模塊/包的導(dǎo)入,可以說是基礎(chǔ)中的基礎(chǔ)。
但這不是唯一的方法,還有 importlib.import_module() 和 __import__() 等。
對于 __import__ ,普通的開發(fā)者,可能就會比較陌生。
和 import 不同的是,__import__ 是一個函數(shù),也正是因為這個原因,使得__import__ 的使用會更加靈活,常常用于框架中,對于插件的動態(tài)加載。
實際上,當(dāng)我們調(diào)用 import 導(dǎo)入模塊時,其內(nèi)部也是調(diào)用了 __import__ ,請看如下兩種導(dǎo)入方法,他們是等價的。
# 使用 import import os # 使用 __import__ os = __import__('os')
通過舉一反三,下面兩種方法同樣也是等價的。
# 使用 import .. as .. import pandas as pd # 使用 __import__ pd = __import__('pandas')
上面我說 __import__ 常常用于插件的動態(tài),事實上也只有它能做到(相對于 import 來說)。
插件通常會位于某一特定的文件夾下,在使用過程中,可能你并不會用到全部的插件,也可能你會新增插件。
如果使用 import 關(guān)鍵字這種硬編碼的方式,顯然太不優(yōu)雅了,當(dāng)你要新增/修改插件的時候,都需要你修改代碼。更合適的做法是,將這些插件以配置的方式,寫在配置文件中,然后由代碼去讀取你的配置,動態(tài)導(dǎo)入你要使用的插件,即靈活又方便,也不容易出錯。
假如我的一個項目中,有 plugin01 、plugin02、plugin03 、plugin04 四個插件,這些插件下都會實現(xiàn)一個核心方法 run() 。但有時候我不想使用全部的插件,只想使用 plugin02、plugin04 ,那我就在配置文件中寫我要使用的兩個插件。
# my.conf custom_plugins=['plugin02', 'plugin04']
那我如何使用動態(tài)加載,并運行他們呢?
# main.py for plugin in conf.custom_plugins: __import__(plugin) sys.modules[plugin].run()
3. 理解模塊的緩存
在一個模塊內(nèi)部重復(fù)引用另一個相同模塊,實際并不會導(dǎo)入兩次,原因是在使用關(guān)鍵字import 導(dǎo)入模塊時,它會先檢索 sys.modules 里是否已經(jīng)載入這個模塊了,如果已經(jīng)載入,則不會再次導(dǎo)入,如果不存在,才會去檢索導(dǎo)入這個模塊。
來實驗一下,在 my_mod02 這個模塊里,我 import 兩次 my_mod01 這個模塊,按邏輯每一次 import 會一次 my_mod01 里的代碼(即打印 in mod01),但是驗證結(jié)果是,只打印了一次。
$ cat my_mod01.py print('in mod01') $ cat my_mod02.py import my_mod01 import my_mod01 $ python my_mod02.py in mod01
該現(xiàn)象的解釋是:因為有 sys.modules 的存在。
sys.modules 是一個字典(key:模塊名,value:模塊對象),它存放著在當(dāng)前 namespace 所有已經(jīng)導(dǎo)入的模塊對象。
# test_module.py import sys print(sys.modules.get('json', 'NotFound')) import json print(sys.modules.get('json', 'NotFound'))
運行結(jié)果如下,可見在 導(dǎo)入后 json 模塊后,sys.modules 才有了 json 模塊的對象。
$ python test_module.py NotFound
由于有緩存的存在,使得我們無法重新載入一個模塊。
但若你想反其道行之,可以借助 importlib 這個神奇的庫來實現(xiàn)。事實也確實有此場景,比如在代碼調(diào)試中,在發(fā)現(xiàn)代碼有異常并修改后,我們通常要重啟服務(wù)再次載入程序。這時候,若有了模塊重載,就無比方便了,修改完代碼后也無需服務(wù)的重啟,就能繼續(xù)調(diào)試。
還是以上面的例子來理解,my_mod02.py 改寫成如下
# my_mod02.py import importlib import my_mod01 importlib.reload(my_mod01)
使用 python3 來執(zhí)行這個模塊,與上面不同的是,這邊執(zhí)行了兩次 my_mod01.py
$ python3 my_mod02.py in mod01 in mod01
4. 查找器與加載器
如果指定名稱的模塊在 sys.modules 找不到,則將發(fā)起調(diào)用 Python 的導(dǎo)入?yún)f(xié)議以查找和加載該模塊。
此協(xié)議由兩個概念性模塊構(gòu)成,即 查找器 和 加載器。
一個 Python 的模塊的導(dǎo)入,其實可以再細(xì)分為兩個過程:
由查找器實現(xiàn)的模塊查找
由加載器實現(xiàn)的模塊加載
4.1 查找器是什么?
查找器(finder),簡單點說,查找器定義了一個模塊查找機制,讓程序知道該如何找到對應(yīng)的模塊。
其實 Python 內(nèi)置了多個默認(rèn)查找器,其存在于 sys.meta_path 中。
但這些查找器對應(yīng)使用者來說,并不是那么重要,因此在 Python 3.3 之前, Python 解釋將其隱藏了,我們稱之為隱式查找器。
# Python 2.7 >>> import sys >>> sys.meta_path [] >>>
由于這點不利于開發(fā)者深入理解 import 機制,在 Python 3.3 后,所有的模塊導(dǎo)入機制都會通過 sys.meta_path 暴露,不會在有任何隱式導(dǎo)入機制。
# Python 3.7 >>> import sys >>> sys.meta_path [, , , ] >>>
觀察一下 Python 默認(rèn)的這幾種查找器 (finder),可以分為三種:
一種知道如何導(dǎo)入內(nèi)置模塊
一種知道如何導(dǎo)入凍結(jié)模塊
一種知道如何導(dǎo)入來自 import path 的模塊 (即 path based finder)。
那我們能不能自已定義一個查找器呢?當(dāng)然可以,你只要:
定義一個實現(xiàn)了 find_module 方法的類(py2和py3均可),或者實現(xiàn) find_loader 類方法(僅 py3 有效),如果找到模塊需要返回一個 loader 對象或者 ModuleSpec 對象(后面會講),沒找到需要返回 None
定義完后,要使用這個查找器,必須注冊它,將其插入在 sys.meta_path 的首位,這樣就能優(yōu)先使用。
import sys class MyFinder(object): @classmethod def find_module(cls, name, path, target=None): print("Importing", name, path, target) # 將在后面定義 return MyLoader() # 由于 finder 是按順序讀取的,所以必須插入在首位 sys.meta_path.insert(0, MyFinder)
查找器可以分為兩種:
object +-- Finder (deprecated) +-- MetaPathFinder +-- PathEntryFinder
這里需要注意的是,在 3.4 版前,查找器會直接返回 加載器(Loader)對象,而在 3.4 版后,查找器則會返回模塊規(guī)格說明(ModuleSpec),其中 包含加載器。
而關(guān)于什么是 加載器 和 模塊規(guī)格說明, 請繼續(xù)往后看。
4.2 加載器是什么?
查找器只負(fù)責(zé)查找定位找模,而真正負(fù)責(zé)加載模塊的,是加載器(loader)。
一般的 loader 必須定義名為 load_module() 的方法。
為什么這里說一般,因為 loader 還分多種:
object +-- Finder (deprecated) | +-- MetaPathFinder | +-- PathEntryFinder +-- Loader +-- ResourceLoader --------+ +-- InspectLoader | +-- ExecutionLoader --+ +-- FileLoader +-- SourceLoader
通過查看源碼可知,不同的加載器的抽象方法各有不同。
加載器通常由一個 查找器 返回。詳情參見 PEP 302。
那如何自定義我們自己的加載器呢?
你只要:
定義一個實現(xiàn)了 load_module 方法的類
對與導(dǎo)入有關(guān)的屬性(點擊查看詳情)進行校驗
創(chuàng)建模塊對象并綁定所有與導(dǎo)入相關(guān)的屬性變量到該模塊上
將此模塊保存到 sys.modules 中(順序很重要,避免遞歸導(dǎo)入)
然后加載模塊(這是核心)
若加載出錯,需要能夠處理拋出異常( ImportError),若加載成功,則返回 module 對象
若你想看具體的例子,可以接著往后看。
4.3 模塊的規(guī)格說明
導(dǎo)入機制在導(dǎo)入期間會使用有關(guān)每個模塊的多種信息,特別是加載之前。大多數(shù)信息都是所有模塊通用的。模塊規(guī)格說明的目的是基于每個模塊來封裝這些導(dǎo)入相關(guān)信息。
模塊的規(guī)格說明會作為模塊對象的 __spec__ 屬性對外公開。有關(guān)模塊規(guī)格的詳細(xì)內(nèi)容請參閱 ModuleSpec。
在 Python 3.4 后,查找器不再返回加載器,而是返回 ModuleSpec 對象,它儲存著更多的信息
模塊名
加載器
模塊絕對路徑
那如何查看一個模塊的 ModuleSpec ?
這邊舉個例子
$ cat my_mod02.py import my_mod01 print(my_mod01.__spec__) $ python3 my_mod02.py in mod01 ModuleSpec(name='my_mod01', loader=<_frozen_importlib_external.SourceFileLoader object at 0x000000000392DBE0>, origin='/home/MING/my_mod01.py')
從 ModuleSpec 中可以看到,加載器是包含在內(nèi)的,那我們?nèi)绻匦录虞d一個模塊,是不是又有了另一種思路了?
來一起驗證一下。
現(xiàn)在有兩個文件:
一個是 my_info.py
# my_info.py name='python'
另一個是:main.py
# main.py import my_info print(my_info.name) # 加一個斷點 import pdb;pdb.set_trace() # 再加載一次 my_info.__spec__.loader.load_module() print(my_info.name)
在 main.py 處,我加了一個斷點,目的是當(dāng)運行到斷點處時,我修改 my_info.py 里的 name 為 ming ,以便驗證重載是否有效?
$ python3 main.py python > /home/MING/main.py(9)() -> my_info.__spec__.loader.load_module() (Pdb) c ming
從結(jié)果來看,重載是有效的。
4.4 導(dǎo)入器是什么?
導(dǎo)入器(importer),也許你在其他文章里會見到它,但其實它并不是個新鮮的東西。
它只是同時實現(xiàn)了查找器和加載器兩種接口的對象,所以你可以說導(dǎo)入器(importer)是查找器(finder),也可以說它是加載器(loader)。
5. 遠(yuǎn)程導(dǎo)入模塊
由于 Python 默認(rèn)的 查找器和加載器 僅支持本地的模塊的導(dǎo)入,并不支持實現(xiàn)遠(yuǎn)程模塊的導(dǎo)入。
為了讓你更好的理解 Python Import Hook 機制,我下面會通過實例演示,如何自己實現(xiàn)遠(yuǎn)程導(dǎo)入模塊的導(dǎo)入器。
5.1 動手實現(xiàn)導(dǎo)入器
當(dāng)導(dǎo)入一個包的時候,Python 解釋器首先會從 sys.meta_path 中拿到查找器列表。
默認(rèn)順序是:內(nèi)建模塊查找器 -> 凍結(jié)模塊查找器 -> 第三方模塊路徑(本地的 sys.path)查找器
若經(jīng)過這三個查找器,仍然無法查找到所需的模塊,則會拋出ImportError異常。
因此要實現(xiàn)遠(yuǎn)程導(dǎo)入模塊,有兩種思路。
一種是實現(xiàn)自己的元路徑導(dǎo)入器;
另一種是編寫一個鉤子,添加到sys.path_hooks里,識別特定的目錄命名模式。
我這里選擇第一種方法來做為示例。
實現(xiàn)導(dǎo)入器,我們需要分別查找器和加載器。
首先是查找器
由源碼得知,路徑查找器分為兩種
MetaPathFinder
PathEntryFinder
這里使用 MetaPathFinder 來進行查找器的編寫。
在 Python 3.4 版本之前,查找器必須實現(xiàn) find_module() 方法,而 Python 3.4+ 版,則推薦使用 find_spec() 方法,但這并不意味著你不能使用 find_module(),但是在沒有 find_spec() 方法時,導(dǎo)入?yún)f(xié)議還是會嘗試 find_module() 方法。
我先舉例下使用 find_module() 該如何寫。
from importlib import abc class UrlMetaFinder(abc.MetaPathFinder): def __init__(self, baseurl): self._baseurl = baseurl def find_module(self, fullname, path=None): if path is None: baseurl = self._baseurl else: # 不是原定義的url就直接返回不存在 if not path.startswith(self._baseurl): return None baseurl = path try: loader = UrlMetaLoader(baseurl) # loader.load_module(fullname) except Exception: return None
若使用 find_spec() ,要注意此方法的調(diào)用需要帶有兩到三個參數(shù)。
第一個是被導(dǎo)入模塊的完整限定名稱,例如 foo.bar.baz。第二個參數(shù)是供模塊搜索使用的路徑條目。對于最高層級模塊,第二個參數(shù)為 None,但對于子模塊或子包,第二個參數(shù)為父包 __path__ 屬性的值。如果相應(yīng)的 __path__ 屬性無法訪問,將引發(fā)ModuleNotFoundError。第三個參數(shù)是一個將被作為稍后加載目標(biāo)的現(xiàn)有模塊對象。導(dǎo)入系統(tǒng)僅會在重加載期間傳入一個目標(biāo)模塊。
from importlib import abc from importlib.machinery import ModuleSpec class UrlMetaFinder(abc.MetaPathFinder): def __init__(self, baseurl): self._baseurl = baseurl def find_spec(self, fullname, path=None, target=None): if path is None: baseurl = self._baseurl else: # 不是原定義的url就直接返回不存在 if not path.startswith(self._baseurl): return None baseurl = path try: loader = UrlMetaLoader(baseurl) return ModuleSpec(fullname, loader, is_package=loader.is_package(fullname)) except Exception: return None
接下來是加載器
由源碼得知,路徑查找器分為兩種
FileLoader
SourceLoader
按理說,兩種加載器都可以實現(xiàn)我們想要的功能,我這里選用 SourceLoader 來示范。
在 SourceLoader 這個抽象類里,有幾個很重要的方法,在你寫實現(xiàn)加載器的時候需要注意
get_code:獲取源代碼,可以根據(jù)自己場景實現(xiàn)實現(xiàn)。
exec_module:執(zhí)行源代碼,并將變量賦值給 module.dict
get_data:抽象方法,必須實現(xiàn),返回指定路徑的字節(jié)碼。
get_filename:抽象方法,必須實現(xiàn),返回文件名
在一些老的博客文章中,你會經(jīng)??吹?加載器 要實現(xiàn) load_module() ,而這個方法早已在 Python 3.4 的時候就被廢棄了,當(dāng)然為了兼容考慮,你若使用 load_module() 也是可以的。
from importlib import abc class UrlMetaLoader(abc.SourceLoader): def __init__(self, baseurl): self.baseurl = baseurl def get_code(self, fullname): f = urllib2.urlopen(self.get_filename(fullname)) return f.read() def load_module(self, fullname): code = self.get_code(fullname) mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) mod.__file__ = self.get_filename(fullname) mod.__loader__ = self mod.__package__ = fullname exec(code, mod.__dict__) return None def get_data(self): pass def execute_module(self, module): pass def get_filename(self, fullname): return self.baseurl + fullname + '.py'
當(dāng)你使用這種舊模式實現(xiàn)自己的加載時,你需要注意兩點,很重要:
execute_module 必須重載,而且不應(yīng)該有任何邏輯,即使它并不是抽象方法。
load_module,需要你在查找器里手動執(zhí)行,才能實現(xiàn)模塊的加載。。
做為替換,你應(yīng)該使用 execute_module() 和 create_module() 。由于基類里已經(jīng)實現(xiàn)了 execute_module 和 create_module(),并且滿足我們的使用場景。我這邊可以不用重復(fù)實現(xiàn)。和舊模式相比,這里也不需要在設(shè)查找器里手動執(zhí)行 execute_module()。
import urllib.request as urllib2 class UrlMetaLoader(importlib.abc.SourceLoader): def __init__(self, baseurl): self.baseurl = baseurl def get_code(self, fullname): f = urllib2.urlopen(self.get_filename(fullname)) return f.read() def get_data(self): pass def get_filename(self, fullname): return self.baseurl + fullname + '.py'
查找器和加載器都有了,別忘了往sys.meta_path 注冊我們自定義的查找器(UrlMetaFinder)。
def install_meta(address): finder = UrlMetaFinder(address) sys.meta_path.append(finder)
所有的代碼都解析完畢后,我們將其整理在一個模塊(my_importer.py)中
# my_importer.py import sys import importlib import urllib.request as urllib2 class UrlMetaFinder(importlib.abc.MetaPathFinder): def __init__(self, baseurl): self._baseurl = baseurl def find_module(self, fullname, path=None): if path is None: baseurl = self._baseurl else: # 不是原定義的url就直接返回不存在 if not path.startswith(self._baseurl): return None baseurl = path try: loader = UrlMetaLoader(baseurl) except Exception: return None class UrlMetaLoader(importlib.abc.SourceLoader): def __init__(self, baseurl): self.baseurl = baseurl def get_code(self, fullname): f = urllib2.urlopen(self.get_filename(fullname)) return f.read() def get_data(self): pass def get_filename(self, fullname): return self.baseurl + fullname + '.py' def install_meta(address): finder = UrlMetaFinder(address) sys.meta_path.append(finder)
5.2 搭建遠(yuǎn)程服務(wù)端
最開始我說了,要實現(xiàn)一個遠(yuǎn)程導(dǎo)入模塊的方法。
我還缺一個在遠(yuǎn)端的服務(wù)器,來存放我的模塊,為了方便,我使用python自帶的http.server 模塊用一條命令即可實現(xiàn)。
$ mkdir httpserver && cd httpserver $ cat>my_info.py一切準(zhǔn)備好,我們就可以驗證了。
>>> from my_importer import install_meta >>> install_meta('http://localhost:12800/') # 往 sys.meta_path 注冊 finder >>> import my_info # 打印ok,說明導(dǎo)入成功 ok >>> my_info.name # 驗證可以取得到變量 'Python編程時光'至此,我實現(xiàn)了一個簡易的可以導(dǎo)入遠(yuǎn)程服務(wù)器上的模塊的導(dǎo)入器。
上述內(nèi)容就是Python的import 機制中如何實現(xiàn)遠(yuǎn)程導(dǎo)入模塊,你們學(xué)到知識或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識儲備,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。
文章題目:Python的import機制中如何實現(xiàn)遠(yuǎn)程導(dǎo)入模塊
文章鏈接:http://weahome.cn/article/pssece.html