1. 程式人生 > >Python import hook

Python import hook

class .html 直接 exce 實現 定位 內置 pat 判斷

轉自http://blog.konghy.cn/2016/10/25/python-import-hook/,這裏有好多好文章

import hook 通常被譯為 探針。我們可以認為每當導入模塊的時候,所觸發的操作就是 import hook。使用 import 的 hook 機制可以讓我們做很多事情,比如加載網絡上的模塊,在導入模塊時對模塊進行修改,自動安裝缺失模塊,上傳審計信息,延遲加載等等。

理解 import hook 需要先了解 Python 導入模塊的過程。

一、 導入過程

Python 通常使用 import 語句來實現類庫的引用,當然內建的 __import__() 函數等都能實現。 import

語句負責做兩件事:

  • 查找模塊
  • 加載模塊到當前名字空間

那麽,一個模塊的導入過程大致可以分為三個步驟:搜索加載名字綁定

1.1 搜索

搜索是整個導入過程的核心,也是最為復雜的一步。這個過程主要是完成查找要引入模塊的功能,查找的過程如下:

  • 1、在緩存 sys.modules 中查找要導入的模塊,若找到則直接返回該模塊對象
  • 2、如果在 sys.modules 中沒有找到相應模塊的緩存,則順序搜索 sys.meta_path,逐個借助其中的 finder 來查找模塊,若找到則加載後返回相應模塊對象。
  • 3、如果以上步驟都沒找到該模塊,則執行默認導入。即如果模塊在一個包中(如import a.b),則以 a.__path__
    為搜索路徑進行查找;如果模塊不在一個包中(如import a),則以 sys.path 為搜索路徑進行查找。
  • 4、如果都未找到,則拋出 ImportError 異常。

查找過程也會檢查?些隱式的 finder 對象,不同的 Python 實現有不同的隱式finder,但是都會有 sys.path_hooks, sys.path_importer_cache 以及sys.path

1.2 加載

對於搜索到的模塊,如果在緩存 sys.modules 中則直接返回模塊對象,否則就需要加載模塊以創建一個模塊對象。加載是對模塊的初始化處理,包括以下步驟:

  • 設置屬性:包括 __name__
    __file____package____loader____path__
  • 編譯源碼:將模塊文件(對於包,則是其對應的 __init__.py 文件)編譯為字節碼(*.pyc 或者 *.pyo),如果字節碼文件已存在且仍然是最新的,則不重新編譯
  • 執行字節碼:執行編譯生成的字節碼(即模塊文件或 __init__.py 文件中的語句)

需要註意的是,加載不只是發生在導入時,還可以發生在 reload 時。

1.3 名字綁定

加載完模塊後,作為最後一步,import 語句會為 導入的對象 綁定名字,並把這些名字加入到當前的名字空間中。其中,導入的對象 根據導入語句的不同有所差異:

  • 如果導入語句為 import obj,則對象 obj 可以是包或者模塊
  • 如果導入語句為 from package import obj,則對象 obj 可以是 package 的子包、package 的屬性或者 package 的子模塊
  • 如果導入語句為 from module import obj,則對象 obj 只能是 module 的屬性

二、模塊緩存

進行搜索時,搜索的第一個地方是便是 sys.modulessys.modules 是一個字典,鍵字為模塊名,鍵值為模塊對象。它包含了從 Python 開始運行起,被導入的所有模塊的一個緩存,包括中間路徑。所以,假如 foo.bar.baz 前期已被導入,那麽,sys.modules 將包含進入 foo,foo.bar 和 foo.bar.baz的入口。每個鍵都有自己的數值,都有對應的模塊對象。也就是說,如果導入 foo.bar.baz 則整個層次結構下的模塊都被加載到了內存。

可以刪除 sys.modules 中對應的的鍵或者將值設置為 None 來使緩存無效。

當啟動 Python 解釋器時,打印一下 sys.modules 中的 key:

>>> import sys
>>> sys.modules.keys()
[‘copy_reg‘, ‘sre_compile‘, ‘_sre‘, ‘encodings‘, ‘site‘, ‘__builtin__‘, ‘sysconfig‘, ‘__main__‘, ‘encodings.encodings‘, ‘abc‘, ‘posixpath‘, ‘_weakrefset‘, ‘errno‘, ‘encodings.codecs‘, ‘sre_constants‘, ‘re‘, ‘_abcoll‘, ‘types‘, ‘_codecs‘, ‘encodings.__builtin__‘, ‘_warnings‘, ‘genericpath‘, ‘stat‘, ‘zipimport‘, ‘_sysconfigdata‘, ‘warnings‘, ‘UserDict‘, ‘encodings.utf_8‘, ‘sys‘, ‘codecs‘, ‘readline‘, ‘_sysconfigdata_nd‘, ‘os.path‘, ‘sitecustomize‘, ‘signal‘, ‘traceback‘, ‘linecache‘, ‘posix‘, ‘encodings.aliases‘, ‘exceptions‘, ‘sre_parse‘, ‘keyrings‘, ‘os‘, ‘_weakref‘]

可以看出一些模塊已經被解釋器導入,但是我們卻不能直接使用這些模塊。這是因為這些模塊還沒有被綁定到當前名字空間,仍然需要執行 import 語句才能完成名字綁定。

三、查找器和加載器

在搜索過程中我們提到 sys.meta_path 中保存了一些 finder 對象。在 Python 查找的時候,如果在 sys.modules 中沒有查找到,就會依次調用 sys.meta_path 中的 finder 對象,即調用導入協議來查找和加載模塊。導入協議包含兩個概念性的對象,查找器(loader) 和 加載器(loader)。sys.meta_path 在任何默認查找程序或 sys.path 之前搜索。默認的情況下,在 Python2 中 sys.meta_path 是一個空列表,並沒有任何 finder 對象;而在 Python3 中則在 Python 中則默認包含三個查找器:第一個知道如何定位內置模塊,第二個知道如何定位凍結模塊,第三個搜索模塊的導入路徑:

[<class _frozen_importlib.BuiltinImporter‘>, <class ‘_frozen_importlib.FrozenImporter‘>, <class ‘_frozen_importlib.PathFinder‘>]

在 Python 中,不僅定義了 finder 和 loader 的概念,還定義了 importor 的概念:

  • 查找器(finder): 決定自己是否能夠通過運用其所知的任何策略找到相應的模塊。在 Python2 中,finder 對象必須實現 find_module() 方法,在 Python3 中必須要實現 find_module() 或者 find_loader() 方法。如果 finder 可以查找到模塊,則會返回一個 loader 對象(在 Python 3.4中,修改為返回一個模塊分支module specs,加載器在導入中仍被使用,但幾乎沒有責任),沒有找到則返回 None。
  • 加載器(loader): 負責加載模塊,它必須實現一個 load_module() 的方法
  • 導入器(importer): 實現了 finder 和 loader 這兩個接口的對象稱為導入器

我們可以想 sys.meta_path 中添加一些自定義的加載器,來實現在加載模塊時對模塊進行修改。例如一個簡單的例子,在每次加載模塊時打印模塊信息:

from __future__ import print_function
import sys

class Watcher(object):

    @classmethod
    def find_module(cls, name, path, target=None):
        print("Importing", name, path, target)
        return None

sys.meta_path.insert(0, Watcher)

import subprocess

輸出結果:

Importing subprocess None None
Importing gc None None
Importing time None None
Importing select None None
Importing fcntl None None
Importing pickle None None
Importing marshal None None
Importing struct None None
Importing _struct None None
Importing org None None
Importing binascii None None
Importing cStringIO None None

四、導入鉤子程序

Python 的導入機制被設計為可擴展的,其基礎的運行機制便是 import hook(導入鉤子程序)。Python 存在兩種導入鉤子程序的形態:一類是上文提到的 meta hook(元鉤子程序), 另一類是 path hook(導入路徑鉤子程序)

在其他任何導入程序運行之前,除了 sys.modules 緩存查找,在導入處理開始時調用元鉤子程序。這就允許元鉤子程序覆蓋 sys.path 處理程序,凍結模塊,或甚至內建模塊。可以通過給 sys.meta_path 添加新的查找器對象來註冊元鉤子程序。

當相關路徑項被沖突時,導入路徑鉤子程序作為 sys.path (或者 package.__path__) 處理程序的一部分被調用。可以通過給 sys.path_hooks 添加新的調用來註冊導入路徑鉤子程序。

sys.path_hooks 是由可被調用的對象組成,它會順序的檢查以決定他們是否可以處理給定的 sys.path 的一項。每個對象會使用 sys.path 項的路徑來作為參數被調用。如果它不能處理該路徑,就必須拋出 ImportError 異常,如果可以,則會返回一個 importer 對象。之後,不會再嘗試其它的 sys.path_hooks 對象,即使前一個 importer 出錯了。

通過 import hook 我們可以根據需求來擴展 Python 的 import 機制。一個簡單的使用導入鉤子的實例,在 import 時判斷庫是否被安裝,否則就自動安裝:

from __future__ import print_function
import sys
import pip
from importlib import import_module


class AutoInstall(object):

    _loaded = set()

    @classmethod
    def find_module(cls, name, path, target=None):
        if path is None and name not in cls._loaded:
            cls._loaded.add(name)
            print("Installing", name)
            installed = pip.main(["install", name])
            if installed == 0:
                return import_module(name)
            else:    
                return None

sys.meta_path.append(AutoInstall)

Python 還提供了一些模塊和函數,可以用來實現簡單的 import hook,主要有一下幾種:

  • __import__: Python 的內置函數;
  • imputil: Python 的 import 工具庫,在 Python2.6 被聲明廢棄,Python3 中徹底移除;
  • imp: Python2 和 Python3 都存在的一個 import 庫;
  • importlib: Python3 中最新添加,backport 到 Python2.7,但只有很小的子集(只有一個 import_module 函數)。

五、site 模塊

site 模塊用於 python 程序啟動的時候,做一些自定義的處理。在 Python 程序運行前,site 模塊會自動導入,並按照如下順序完成初始化工作:

  • sys.prefixsys.exec_prefixlib/pythonX.Y/site-packages 合成 module 的 search path。加入sys.path。eg: /home/jay/env/tornado/lib/python2.7/site-packages
  • 在添加的路徑下尋找 pth 文件。 該文件中描述了添加到 sys.path 的子文件夾路徑。
  • import sitecustomize, sitecustomize 內部可以做任意的設置。
  • import usercustomize, usercustomize 一般放在用戶的 path 環境下, 如: /home/jay/.local/lib/python2.7/site-packages/usercustomize, 其內部可以做任意的設置。

site 模塊的本質可以說是補充 sys.path 路徑,協助解釋器預配置第三方模塊目錄。所以可以設置特殊的 sitecustomize.py 或者 usercustomize.py 文件, 在 python 代碼執行之前,添加 import hook

六、導入搜索路徑

Python 在 import 時會在系統中搜索模塊或者包所在的位置,sys.path 變量中保存了所有可搜索的庫路徑,它是一個路徑名的列表,其中的路徑主要分為以下幾部分:

  • 程序主目錄(默認定義): 如果是以腳本方式啟動的程序,則為啟動腳本所在目錄;如果在交互式解釋器中,則為當前目錄;
  • PYTHONPATH目錄(可選擴展): 以 os.pathsep 分隔的多個目錄名,即環境變量 os.environ[‘PYTHONPATH‘](類似 shell 環境變量 PATH);
  • 標準庫目錄(默認定義): Python 標準庫所在目錄(與安裝目錄有關);
  • .pth文件目錄(可選擴展): 以 “.pth” 為後綴的文件,其中列有一些目錄名(每行一個目錄名)。

因此如果想要添加庫的搜索路徑,可以有如下方法:

  • 直接修改 sys.path 列表
  • 使用 PYTHONPATH 擴展
  • 使用 .pth 文件擴展

七、重新加載

關於 import,還有一點非常關鍵:加載只在第一次導入時發生。Python 這樣設計的目的是因為加載是個代價高昂的操作。

通常情況下,如果模塊沒有被修改,這正是我們想要的行為;但如果我們修改了某個模塊,重復導入不會重新加載該模塊,從而無法起到更新模塊的作用。有時候我們希望在 運行時(即不終止程序運行的同時),達到即時更新模塊的目的,內建函數 reload() 提供了這種 重新加載 機制(在 Python3 中被挪到了 imp 模塊下)。

關於 reload 與 import 的不同:

  • import 是語句,而 reload 是函數
  • import 使用 模塊名,而 reload 使用 模塊對象(即已被import語句成功導入的模塊)

重新加載 reload(module) 有以下幾個特點:

  • 會重新編譯和執行模塊文件中的頂層語句
  • 會更新模塊的名字空間(字典 M.__dict__):覆蓋相同的名字(舊的有,新的也有),保留缺失的名字(舊的有,新的沒有),添加新增的名字(舊的沒有,新的有)
  • 對於由 import M 語句導入的模塊 M:調用 reload(M) 後,M.x 為 新模塊 的屬性 x(因為更新M後,會影響M.x的求值結果)
  • 對於由 from M import x 語句導入的屬性 x:調用 reload(M) 後,x 仍然是 舊模塊 的屬性 x(因為更新M後,不會影響x的求值結果)
  • 如果在調用 reload(M) 後,重新執行 import M(或者from M import x)語句,那麽 M.x(或者x)為 新模塊 的屬性 x

八、參考資料

  • https://github.com/Liuchang0812/slides/tree/master/pycon2015cn
  • http://www.cnblogs.com/russellluo/p/3328683.html
  • http://wiki.jikexueyuan.com/project/python-language-reference/import.html

Python import hook