溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

深入探討 Python 的 import 機(jī)制:實(shí)現(xiàn)遠(yuǎn)程導(dǎo)入模塊

發(fā)布時(shí)間:2020-06-21 00:36:49 來源:網(wǎng)絡(luò) 閱讀:340 作者:PythonTime 欄目:編程語言

1.24 深入探討 Python 的 import 機(jī)制:實(shí)現(xiàn)遠(yuǎn)程導(dǎo)入模塊

所謂的模塊導(dǎo)入( import ),是指在一個(gè)模塊中使用另一個(gè)模塊的代碼的操作,它有利于代碼的復(fù)用。

在 Python 中使用 import 關(guān)鍵字來實(shí)現(xiàn)這個(gè)操作,但不是唯一的方法,還有 importlib.import_module()__import__() 等。

也許你看到這個(gè)標(biāo)題,會(huì)說我怎么會(huì)發(fā)這么基礎(chǔ)的文章?

與此相反。恰恰我覺得這篇文章的內(nèi)容可以算是 Python 的進(jìn)階技能,會(huì)深入地探討并以真實(shí)案例講解 Python import Hook 的知識(shí)點(diǎn)。

當(dāng)然為了使文章更系統(tǒng)、全面,前面會(huì)有小篇幅講解基礎(chǔ)知識(shí)點(diǎn),但請你有耐心的往后讀下去,因?yàn)楹竺娌攀潜酒恼碌木A所在,希望你不要錯(cuò)過。

深入探討 Python 的 import 機(jī)制:實(shí)現(xiàn)遠(yuǎn)程導(dǎo)入模塊

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:是一個(gè)帶有 __init__.py 文件的文件夾,此文件夾下可包含其他子包,或者模塊
  • Namespace packages

關(guān)于 Namespace packages,有的人會(huì)比較陌生,我這里摘抄官方文檔的一段說明來解釋一下。

Namespace packages 是由多個(gè) 部分 構(gòu)成的,每個(gè)部分為父包增加一個(gè)子包。 各個(gè)部分可能處于文件系統(tǒng)的不同位置。 部分也可能處于 zip 文件中、網(wǎng)絡(luò)上,或者 Python 在導(dǎo)入期間可以搜索的其他地方。 命名空間包并不一定會(huì)直接對應(yīng)到文件系統(tǒng)中的對象;它們有可能是無實(shí)體表示的虛擬模塊。

命名空間包的 __path__ 屬性不使用普通的列表。 而是使用定制的可迭代類型,如果其父包的路徑 (或者最高層級包的 sys.path) 發(fā)生改變,這種對象會(huì)在該包內(nèi)的下一次導(dǎo)入嘗試時(shí)自動(dòng)執(zhí)行新的對包部分的搜索。

命名空間包沒有 parent/__init__.py 文件。 實(shí)際上,在導(dǎo)入搜索期間可能找到多個(gè) parent 目錄,每個(gè)都由不同的部分所提供。 因此 parent/one 的物理位置不一定與 parent/two 相鄰。 在這種情況下,Python 將為頂級的 parent 包創(chuàng)建一個(gè)命名空間包,無論是它本身還是它的某個(gè)子包被導(dǎo)入。

1.2 相對/絕對導(dǎo)入

當(dāng)我們 import 導(dǎo)入模塊或包時(shí),Python 提供兩種導(dǎo)入方式:

  • 相對導(dǎo)入(relative import ):from . import B 或 from ..A import B,其中.表示當(dāng)前模塊,..表示上層模塊
  • 絕對導(dǎo)入(absolute import):import foo.bar 或者 form foo import bar

你可以根據(jù)實(shí)際需要進(jìn)行選擇,但有必要說明的是,在早期的版本( Python2.6 之前),Python 默認(rèn)使用的相對導(dǎo)入。而后來的版本中( Python2.6 之后),都以絕對導(dǎo)入為默認(rèn)使用的導(dǎo)入方式。

使用絕對路徑和相對路徑各有利弊:

  • 當(dāng)你在開發(fā)維護(hù)自己的項(xiàng)目時(shí),應(yīng)當(dāng)使用相對路徑導(dǎo)入,這樣可以避免硬編碼帶來的麻煩。
  • 而使用絕對路徑,會(huì)讓你模塊導(dǎo)入結(jié)構(gòu)更加清晰,而且也避免了重名的包沖突而導(dǎo)入錯(cuò)誤。

1.3 導(dǎo)入的標(biāo)準(zhǔn)寫法

在 PEP8 中對模塊的導(dǎo)入提出了要求,遵守 PEP8規(guī)范能讓你的代碼更具有可讀性,我這邊也列一下:

  • 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)該按照順序排列,每組之間用一個(gè)空格分隔,按照內(nèi)置模塊,第三方模塊,自己所寫的模塊調(diào)用順序,同時(shí)每組內(nèi)部按照字母表順序排列
# 內(nèi)置模塊
import os
import sys

# 第三方模塊
import flask

# 本地模塊
from foo import bar

1.4 幾個(gè)有用的 sys 變量

sys.path 可以列出 Python 模塊查找的目錄列表

>>> import sys
>>> from pprint import pprint
>>> pprint(sys.path)
['',
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip',
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6',
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload',
 '/Users/MING/Library/Python/3.6/lib/python/site-packages',
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']
>>>

sys.meta_path 存放的是所有的查找器。

>>> import sys
>>> from pprint import pprint
>>> pprint(sys.meta_path)
[<class '_frozen_importlib.BuiltinImporter'>,
 <class '_frozen_importlib.FrozenImporter'>,
 <class '_frozen_importlib_external.PathFinder'>]

sys.path_importer_cachesys.path 會(huì)更大點(diǎn), 因?yàn)樗鼤?huì)為所有被加載代碼的目錄記錄它們的查找器。 這包括包的子目錄,這些通常在 sys.path 中是不存在的。

>>> import sys
>>> from pprint import pprint
>>> pprint(sys.path_importer_cache)
{'/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6'),
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/collections': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/collections'),
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/encodings': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/encodings'),
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload'),
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages': FileFinder('/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages'),
 '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip': None,
 '/Users/MING': FileFinder('/Users/MING'),
 '/Users/MING/Library/Python/3.6/lib/python/site-packages': FileFinder('/Users/MING/Library/Python/3.6/lib/python/site-packages')}

2. __import__ 的妙用

import 關(guān)鍵字的使用,可以說是基礎(chǔ)中的基礎(chǔ)。

但這不是模塊唯一的方法,還有 importlib.import_module()__import__() 等。

和 import 不同的是,__import__ 是一個(gè)函數(shù),也正是因?yàn)檫@個(gè)原因,使得 __import__ 的使用會(huì)更加靈活,常常用于框架中,對于插件的動(dòng)態(tài)加載。

實(shí)際上,當(dāng)我們調(diào)用 import 導(dǎo)入模塊時(shí),其內(nèi)部也是調(diào)用了 __import__ ,請看如下兩種導(dǎo)入方法,他們是等價(jià)的。

# 使用 import
import os

# 使用 __import__
os = __import__('os')

通過舉一反三,下面兩種方法同樣也是等價(jià)的。

# 使用 import .. as ..
import pandas as pd

# 使用 __import__
pd = __import__('pandas')

上面我說 __import__ 常常用于插件的動(dòng)態(tài),事實(shí)上也只有它能做到(相對于 import 來說)。

插件通常會(huì)位于某一特定的文件夾下,在使用過程中,可能你并不會(huì)用到全部的插件,也可能你會(huì)新增插件。

如果使用 import 關(guān)鍵字這種硬編碼的方式,顯然太不優(yōu)雅了,當(dāng)你要新增/修改插件的時(shí)候,都需要你修改代碼。更合適的做法是,將這些插件以配置的方式,寫在配置文件中,然后由代碼去讀取你的配置,動(dòng)態(tài)導(dǎo)入你要使用的插件,即靈活又方便,也不容易出錯(cuò)。

假如我的一個(gè)項(xiàng)目中,有 plugin01 、plugin02plugin03 、plugin04 四個(gè)插件,這些插件下都會(huì)實(shí)現(xiàn)一個(gè)核心方法 run() 。但有時(shí)候我不想使用全部的插件,只想使用 plugin02plugin04 ,那我就在配置文件中寫我要使用的兩個(gè)插件。

# my.conf
custom_plugins=['plugin02', 'plugin04']

那我如何使用動(dòng)態(tài)加載,并運(yùn)行他們呢?

# main.py

for plugin in conf.custom_plugins:
    __import__(plugin)
    sys.modules[plugin].run()

3. 理解模塊的緩存

在一個(gè)模塊內(nèi)部重復(fù)引用另一個(gè)相同模塊,實(shí)際并不會(huì)導(dǎo)入兩次,原因是在使用關(guān)鍵字 import 導(dǎo)入模塊時(shí),它會(huì)先檢索 sys.modules 里是否已經(jīng)載入這個(gè)模塊了,如果已經(jīng)載入,則不會(huì)再次導(dǎo)入,如果不存在,才會(huì)去檢索導(dǎo)入這個(gè)模塊。

來實(shí)驗(yàn)一下,在 my_mod02 這個(gè)模塊里,我 import 兩次 my_mod01 這個(gè)模塊,按邏輯每一次 import 會(huì)一次 my_mod01 里的代碼(即打印 in mod01),但是驗(yàn)證結(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)象的解釋是:因?yàn)橛?sys.modules 的存在。

sys.modules 是一個(gè)字典(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'))

運(yùn)行結(jié)果如下,可見在 導(dǎo)入后 json 模塊后,sys.modules 才有了 json 模塊的對象。

$ python test_module.py
NotFound
<module 'json' from 'C:\Python27\lib\json\__init__.pyc'>

由于有緩存的存在,使得我們無法重新載入一個(gè)模塊。

但若你想反其道行之,可以借助 importlib 這個(gè)神奇的庫來實(shí)現(xiàn)。事實(shí)也確實(shí)有此場景,比如在代碼調(diào)試中,在發(fā)現(xiàn)代碼有異常并修改后,我們通常要重啟服務(wù)再次載入程序。這時(shí)候,若有了模塊重載,就無比方便了,修改完代碼后也無需服務(wù)的重啟,就能繼續(xù)調(diào)試。

還是以上面的例子來理解,my_mod02.py 改寫成如下

# my_mod02.py

import importlib
import my_mod01
importlib.reload(my_mod01)

使用 python3 來執(zhí)行這個(gè)模塊,與上面不同的是,這邊執(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è)概念性模塊構(gòu)成,即 查找器加載器

一個(gè) Python 的模塊的導(dǎo)入,其實(shí)可以再細(xì)分為兩個(gè)過程:

  1. 由查找器實(shí)現(xiàn)的模塊查找
  2. 由加載器實(shí)現(xiàn)的模塊加載

4.1 查找器是什么?

查找器(finder),簡單點(diǎn)說,查找器定義了一個(gè)模塊查找機(jī)制,讓程序知道該如何找到對應(yīng)的模塊。

其實(shí) Python 內(nèi)置了多個(gè)默認(rèn)查找器,其存在于 sys.meta_path 中。

但這些查找器對應(yīng)使用者來說,并不是那么重要,因此在 Python 3.3 之前, Python 解釋將其隱藏了,我們稱之為隱式查找器。

# Python 2.7
>>> import sys
>>> sys.meta_path
[]
>>> 

由于這點(diǎn)不利于開發(fā)者深入理解 import 機(jī)制,在 Python 3.3 后,所有的模塊導(dǎo)入機(jī)制都會(huì)通過 sys.meta_path 暴露,不會(huì)在有任何隱式導(dǎo)入機(jī)制。

# Python 3.6
>>> import sys
>>> from pprint import pprint
>>> pprint(sys.meta_path)
[<class '_frozen_importlib.BuiltinImporter'>,
 <class '_frozen_importlib.FrozenImporter'>,
 <class '_frozen_importlib_external.PathFinder'>]

觀察一下 Python 默認(rèn)的這幾種查找器 (finder),可以分為三種:

  • 一種知道如何導(dǎo)入內(nèi)置模塊
  • 一種知道如何導(dǎo)入凍結(jié)模塊
  • 一種知道如何導(dǎo)入來自 import path 的模塊 (即 path based finder)。

那我們能不能自已定義一個(gè)查找器呢?當(dāng)然可以,你只要

  • 定義一個(gè)實(shí)現(xiàn)了 find_module 方法的類(py2和py3均可),或者實(shí)現(xiàn) find_loader 類方法(僅 py3 有效),如果找到模塊需要返回一個(gè) loader 對象或者 ModuleSpec 對象(后面會(huì)講),沒找到需要返回 None
  • 定義完后,要使用這個(gè)查找器,必須注冊它,將其插入在 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 版前,查找器會(huì)直接返回 加載器(Loader)對象,而在 3.4 版后,查找器則會(huì)返回模塊規(guī)格說明(ModuleSpec),其中 包含加載器。

而關(guān)于什么是 加載器 和 模塊規(guī)格說明, 請繼續(xù)往后看。

4.2 加載器是什么?

查找器只負(fù)責(zé)查找定位找模,而真正負(fù)責(zé)加載模塊的,是加載器(loader)。

一般的 loader 必須定義名為 load_module() 的方法。

為什么這里說一般,因?yàn)?loader 還分多種:

object
 +-- Finder (deprecated)
 |    +-- MetaPathFinder
 |    +-- PathEntryFinder
 +-- Loader
      +-- ResourceLoader --------+
      +-- InspectLoader          |
           +-- ExecutionLoader --+
                                 +-- FileLoader
                                 +-- SourceLoader

通過查看源碼可知,不同的加載器的抽象方法各有不同。

加載器通常由一個(gè) finder 返回。詳情參見 PEP 302,對于 abstract base class 可參見 importlib.abc.Loader。

那如何自定義我們自己的加載器呢?

你只要

  • 定義一個(gè)實(shí)現(xiàn)了 load_module 方法的類
  • 對與導(dǎo)入有關(guān)的屬性(點(diǎn)擊查看詳情)進(jìn)行校驗(yàn)
  • 創(chuàng)建模塊對象并綁定所有與導(dǎo)入相關(guān)的屬性變量到該模塊上
  • 將此模塊保存到 sys.modules 中(順序很重要,避免遞歸導(dǎo)入)
  • 然后加載模塊(這是核心)
  • 若加載出錯(cuò),需要能夠處理拋出異常( ImportError)
  • 若加載成功,則返回 module 對象

若你想看具體的例子,可以接著往后看。

4.3 模塊規(guī)格說明

導(dǎo)入機(jī)制在導(dǎo)入期間會(huì)使用有關(guān)每個(gè)模塊的多種信息,特別是加載之前。 大多數(shù)信息都是所有模塊通用的。 模塊規(guī)格說明的目的是基于每個(gè)模塊來封裝這些導(dǎo)入相關(guān)信息。

模塊的規(guī)格說明會(huì)作為模塊對象的 __spec__ 屬性對外公開。 有關(guān)模塊規(guī)格的詳細(xì)內(nèi)容請參閱 ModuleSpec

在 Python 3.4 后,查找器不再返回加載器,而是返回 ModuleSpec 對象,它儲(chǔ)存著更多的信息

  • 模塊名
  • 加載器
  • 模塊絕對路徑

那如何查看一個(gè)模塊的 ModuleSpec ?

這邊舉個(gè)例子

$ 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一個(gè)模塊,是不是又有了另一種思路了?

來一起驗(yàn)證一下。

現(xiàn)在有兩個(gè)文件:

一個(gè)是 my_info.py

# my_info.py
name='wangbm'

另一個(gè)是:main.py

# main.py
import my_info

print(my_info.name)

# 加一個(gè)斷點(diǎn)
import pdb;pdb.set_trace()

# 再加載一次
my_info.__spec__.loader.load_module()

print(my_info.name)

main.py 處,我加了一個(gè)斷點(diǎn),目的是當(dāng)運(yùn)行到斷點(diǎn)處時(shí),我修改 my_info.py 里的 name 為 ming ,以便驗(yàn)證重載是否有效?

$ python3 main.py
wangbm
> /home/MING/main.py(9)<module>()
-> my_info.__spec__.loader.load_module()
(Pdb) c
ming

從結(jié)果來看,重載是有效的。

4.4 導(dǎo)入器是什么?

導(dǎo)入器(importer),也許你在其他文章里會(huì)見到它,但其實(shí)它并不是個(gè)新鮮的東西。

它只是同時(shí)實(shí)現(xiàn)了查找器和加載器兩種接口的對象,所以你可以說導(dǎo)入器(importer)是查找器(finder),也可以說它是加載器(loader)。

5. 遠(yuǎn)程導(dǎo)入模塊

由于 Python 默認(rèn)的 查找器和加載器 僅支持本地的模塊的導(dǎo)入,并不支持實(shí)現(xiàn)遠(yuǎn)程模塊的導(dǎo)入。

為了讓你更好的理解 Python Import Hook 機(jī)制,我下面會(huì)通過實(shí)例演示,如何自己實(shí)現(xiàn)遠(yuǎn)程導(dǎo)入模塊的導(dǎo)入器。

5.1 動(dòng)手實(shí)現(xiàn)導(dǎo)入器

當(dāng)導(dǎo)入一個(gè)包的時(shí)候,Python 解釋器首先會(huì)從 sys.meta_path 中拿到查找器列表。

默認(rèn)順序是:內(nèi)建模塊查找器 -> 凍結(jié)模塊查找器 -> 第三方模塊路徑(本地的 sys.path)查找器

若經(jīng)過這三個(gè)查找器,仍然無法查找到所需的模塊,則會(huì)拋出ImportError異常。

因此要實(shí)現(xiàn)遠(yuǎn)程導(dǎo)入模塊,有兩種思路。

  • 一種是實(shí)現(xiàn)自己的元路徑導(dǎo)入器;
  • 另一種是編寫一個(gè)鉤子,添加到sys.path_hooks里,識(shí)別特定的目錄命名模式。

我這里選擇第一種方法來做為示例。

實(shí)現(xiàn)導(dǎo)入器,我們需要分別查找器和加載器。

首先是查找器

由源碼得知,路徑查找器分為兩種

  • MetaPathFinder
  • PathEntryFinder

這里使用 MetaPathFinder 來進(jìn)行查找器的編寫。

在 Python 3.4 版本之前,查找器必須實(shí)現(xiàn) find_module() 方法,而 Python 3.4+ 版,則推薦使用 find_spec() 方法,但這并不意味著你不能使用 find_module(),但是在沒有 find_spec() 方法時(shí),導(dǎo)入?yún)f(xié)議還是會(huì)嘗試 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)
            return loader
        except Exception:
            return None

若使用 find_spec() ,要注意此方法的調(diào)用需要帶有兩到三個(gè)參數(shù)。

第一個(gè)是被導(dǎo)入模塊的完整限定名稱,例如 foo.bar.baz。 第二個(gè)參數(shù)是供模塊搜索使用的路徑條目。 對于最高層級模塊,第二個(gè)參數(shù)為 None,但對于子模塊或子包,第二個(gè)參數(shù)為父包 __path__ 屬性的值。 如果相應(yīng)的 __path__ 屬性無法訪問,將引發(fā) ModuleNotFoundError。 第三個(gè)參數(shù)是一個(gè)將被作為稍后加載目標(biāo)的現(xiàn)有模塊對象。 導(dǎo)入系統(tǒng)僅會(huì)在重加載期間傳入一個(gè)目標(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

按理說,兩種加載器都可以實(shí)現(xiàn)我們想要的功能,我這里選用 SourceLoader 來示范。

在 SourceLoader 這個(gè)抽象類里,有幾個(gè)很重要的方法,在你寫實(shí)現(xiàn)加載器的時(shí)候需要注意

  • get_code:獲取源代碼,可以根據(jù)自己場景實(shí)現(xiàn)實(shí)現(xiàn)。
  • exec_module:執(zhí)行源代碼,并將變量賦值給 module.__dict__
  • get_data:抽象方法,必須實(shí)現(xiàn),返回指定路徑的字節(jié)碼。
  • get_filename:抽象方法,必須實(shí)現(xiàn),返回文件名

在一些老的博客文章中,你會(huì)經(jīng)??吹?加載器 要實(shí)現(xiàn) load_module() ,而這個(gè)方法早已在 Python 3.4 的時(shí)候就被廢棄了,當(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)你使用這種舊模式實(shí)現(xiàn)自己的加載時(shí),你需要注意兩點(diǎn),很重要:

  • execute_module 必須重載,而且不應(yīng)該有任何邏輯,即使它并不是抽象方法。
  • load_module,需要你在查找器里手動(dòng)執(zhí)行,才能實(shí)現(xiàn)模塊的加載。。

做為替換,你應(yīng)該使用 execute_module()create_module() 。由于基類里已經(jīng)實(shí)現(xiàn)了 execute_modulecreate_module(),并且滿足我們的使用場景。我這邊可以不用重復(fù)實(shí)現(xiàn)。和舊模式相比,這里也不需要在設(shè)查找器里手動(dòng)執(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)

所有的代碼都解析完畢后,我們將其整理在一個(gè)模塊(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)
            return loader
        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ù)端

最開始我說了,要實(shí)現(xiàn)一個(gè)遠(yuǎn)程導(dǎo)入模塊的方法。

我還缺一個(gè)在遠(yuǎn)端的服務(wù)器,來存放我的模塊,為了方便,我使用python自帶的 http.server 模塊用一條命令即可實(shí)現(xiàn)。

$ mkdir httpserver && cd httpserver
$ cat>my_info.py<EOF
name='wangbm'
print('ok')
EOF

$ cat my_info.py
name='wangbm'
print('ok')
$ 
$ python3 -m http.server 12800
Serving HTTP on 0.0.0.0 port 12800 (http://0.0.0.0:12800/) ...
...

一切準(zhǔn)備好,我們就可以驗(yà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  # 驗(yàn)證可以取得到變量
'wangbm'

至此,我實(shí)現(xiàn)了一個(gè)簡易的可以導(dǎo)入遠(yuǎn)程服務(wù)器上的模塊的導(dǎo)入器。

參考文檔

  • https://docs.python.org/zh-cn/3/reference/import.html
  • https://docs.python.org/zh-cn/3/library/importlib.html#module-importlib.abc
  • https://python3-cookbook.readthedocs.io/zh_CN/latest/c10/p11_load_modules_from_remote_machine_by_hooks.html

深入探討 Python 的 import 機(jī)制:實(shí)現(xiàn)遠(yuǎn)程導(dǎo)入模塊

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI