初窺 Python 的 import 機制

2021-03-02 Python爬蟲與數據挖掘

回復「書籍」即可獲贈Python從入門到進階共10本電子書

本文適合有 Python 基礎的小夥伴進階學習。

作者:pwwang

一、前言

本文基於開源項目:

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 語句會執行兩步操作:

搜索步驟實際上是通過 __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 是一個存放元路徑查找器的列表。它有三個默認的查找器:

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 機制靈活而強大。以上的介紹大部分是基於官方文檔,以及較新的 Python 3.6+ 版本。由於篇幅,還有很多細節並沒有包含其中,例如子模塊的加載、模塊代碼的緩存機制等等。

------------------- End -------------------


往期精彩文章推薦:

歡迎大家點讚,留言,轉發,轉載,感謝大家的相伴與支持

想加入Python學習群請在後臺回復【入群

萬水千山總是情,點個【在看】行不行

/今日留言主題/

隨便說一兩句吧~

相關焦點

  • 深入探討 Python 的 import 機制
    還是以上面的例子來理解,my_mod02.py 改寫成如下# my_mod02.pyimport importlibimport my_mod01importlib.reload(my_mod01)使用 python3 來執行這個模塊,與上面不同的是,這邊執行了兩次 my_mod01.py$
  • 詳解Python import機制(上):import中的基本概念
    作者 |  ayuliao來源 | hackpython(ID: hackpython)簡介簡單來看,import機制可以導入我們需要使用的庫,避免代碼重複,使用方便,可謂是編寫Python時最常使用寫法,但我們了解import嗎?
  • Python Import 機制與拓展——劉暢@PyCon 2015 China
    在真正的將python應用到實際的項目中,你會遇到一些無法避免的問題。最讓人困惑不解的問題有二類,一個 編碼問題,另一個則是引用問題。本文主要討論關於Python中import的機制、實現、以及介紹一些有意思的Python Hooks。
  • 詳解Python import機制(一):import中的基本概念
    ,import 機制可以導入我們需要使用的庫,避免代碼重複,使用方便,可謂是編寫 Python 時最常使用寫法,但我們了解 import 嗎?impimport 其實有很多容易混淆的概念以及可以實現很多非常有趣的玩法,本篇文章拋磚引玉,聊聊 import需注意,Python2 與 Python3 的 import 機制有較大差別,主要體現在兩點:簡單而言,Python3.7 與 Python2.7 在 import 機制上有較大差異,這裡以Python3.7 為基準進行討論。
  • python基礎--自定義模塊、import、from......import......
    如果你退出python解釋器然後重新進入,那麼你之前定義的函數或者變量都將丟失,因此我們通常將程序寫到文件中以便永久保存下來,需要時就通過python test.py方式去執行,此時test.py被稱為腳本script。所以,腳本就是一個python文件,比如你之前寫的購物車,模擬博客園登錄系統的文件等等。3.模塊的分類Python語言中,模塊分為三類。
  • python 模塊相互import
    模塊A中import B,而在模塊B中import A。這時會怎麼樣呢?
  • 詳解Python中的import的用法
    import語句用來導入其他python文件(稱為模塊module),使用該模塊裡定義的類、方法或者變量,從而達到代碼復用的目的。為了方便說明,我們用實例來說明import的用法,讀者朋友可以跟著嘗試(嘗試時建議使用python3,python2和python3在import的表現有差異,之後會提到)。
  • 你對Python裡的import一無所知
    gt;> myos<module 'sep' from '/usr/lib64/python2.7/os.pyc'>>>> myos.getcwd()'/home/wangbm'從 python 3 開始,內建的 reload 函數被移到了 imp 模塊中。
  • Python ImportError 解決思路
    辛辛苦苦安裝完了python3.8,最後再運行的時候會出現ImportError: No module named configparser的報錯,參考了很多資料,未能解決問題,後來突然想到原來之前安裝過python2.7版本,對於python來說2.x版本與3.x版本中模塊的名稱是不一樣的
  • Python import 導入上一級模塊
    1.導入同級模塊python導入同級模塊(在同一個文件夾中的py文件)直接導入即可。import xxx 如在file4.py中想引入import上級目錄下的file1.py:import sys sys.path.append("..")
  • Python中from import和import的區別?沒有比這更好的回答了
    ps:使用的python3.5版本。
  • Python 的 import 居然這麼有料
    今天給大家分享一篇文章,非常詳細剖析了 Python 的 import 機制,是一篇難得的好文。一、前言 本文基於開源項目:https://github.com/pwwang/python-import-system補充擴展講解,希望能夠讓讀者一文搞懂 Python 的 import 機制。1.1 什麼是 import 機制?
  • 如何 Import 自定義的 Python 模塊?
    (給Python開發者加星標,提升Python技能)來源:Be_meltinghttps://blog.csdn.net/lys_828/article/details/106176229【導語】:實際工作中,經常要用的功能能不能像導入python
  • 1 小時逼瘋面試者:聊聊 Python Import System?
    在Python 2.6之前,Python的默認import機制是「relative import(相對導入)」,而之後則改成了「absolute import(絕對導入)」,那這兩個不同的導入機制該怎麼理解呢?
  • Python入門基礎之導入問題:from import 與import 詳解
    在python中導入模塊絕對是我們最最常用的功能,基本每個py文件中都會有import或者是from import語句。可是,這兩種導入方法有什麼不同,又該怎麼用呢?今天就好好來分析一下。Import工作方式:其中,導入程序搜索的目錄如下:import解釋:解釋器執行到import語句, 如果在搜索路徑中找到了指定的模塊, 就會加載它。
  • 慢步python,說說import,引用功能代碼(功能庫、py文件代碼)
    今天慢步休息,繼續學習python語言。其實慢步已經將基礎和必要的python語言知識點寫了。今天想說說importimport是python語言的保留字,它能實現引用當前程序之外已有的功能代碼。python語言像積木,你可以根據你想要的功能,編寫一系列的代碼。比如筆者之前編寫的《word文檔標題置換》。
  • 《面試官一個小時逼瘋面試者》之聊聊Python Import System?
    在Python 2.6之前,Python的默認import機制是「relative import(相對導入)」,而之後則改成了「absolute import(絕對導入)」,那這兩個不同的導入機制該怎麼理解呢?
  • Python時間運算的詳細機制初探討
    博客地址:http://coderselftrain.com/archives/本篇共2部分,上篇我們初步探討Python時間運算的機制,下篇為一句話快速運用Python的datetime進行時間運算的技巧。
  • Python模塊import本質是什麼?
    寫這篇文章主要是對Python的import模塊或包的機制有了更深層級的一個理解,也在具體工作中得到了一點實踐,這種思考是由上一篇文章
  • 學了半天,import 到底在幹啥?
    噢,對了~是用的import xxx這個語句。當然多數時候我們感知不到這個過程,因為我們就是一個import走天下:import sysimport osimport requests這個時候我們指定的模塊名,實際上也是指定的稍後用來調用相應模塊的對象名稱