日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

python导入机制及importlib模块

發(fā)布時間:2024/1/23 python 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 python导入机制及importlib模块 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

文章目錄

  • 寫在篇前
  • import 關鍵字
  • 先導概念
    • namespace & scope
    • Module & Packages
      • module
      • packages
        • regular package
        • namespace package
  • importlib
    • Loaders & Finders
    • import hooks
    • importlib.abc
    • importlib.resources
  • 參考資料

寫在篇前

這篇博客的雛形,嚴格來講,在我腦海中浮現(xiàn)已有近一年之久,起源于我之前在寫一個python模塊并用jupyter notebook測試時發(fā)現(xiàn),當在一個session中通過import導入模塊,修改模塊再次通過import導入該模塊時,模塊修改并不會生效。至此,通過一番研究發(fā)現(xiàn),python 導入機制(import machinery)對于我們理解python這門語言,有著至關重要的作用。因此,本篇博客主要探討 python import machinery原理及其背后的應用。

import 關鍵字

關鍵字import大家肯定都非常熟悉,我們可以通過import導入不同功能的模塊 (modules)包 (packages)。在這里,顯然import關鍵字本身不是我們的重點。因此,我們僅以簡略的形式介紹一下python import語句的使用,后面來重點關注import語句背后的導入機制及更深層次的用法。import語句導入主要包括以下形式:

import <module_name> from <module_name> import <name(s)> from <module_name> import <name> as <alt_name> import <module_name> as <alt_name> import <module_name1>, <module_name2>, <module_name3>, ... # 為了代碼規(guī)范,不推薦該使用方式 from <module_name> import * # 不推薦

我們舉個例子, 假設我們寫了一個模塊mod.py,在該模塊中定義了一個字符串s,一個list a, 一個函數(shù)foo和一個類Foo:

s = "If Comrade Napoleon says it, it must be right." a = [100, 200, 300]def foo(arg):print(f'arg = {arg}')class Foo:pass

通過import導入mod.py模塊,可以使用dir()函數(shù)查看導入前后當前命名空間變量的變化:

>>> dir() ['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__'] >>> import mod >>> dir() ['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'mod'] >>> mod.__file__ '/Users/jeffery/mod.py' >>> mod.__name__ 'mod' >>> mod.s 'If Comrade Napoleon says it, it must be right.' >>> mod.a [100, 200, 300] >>> mod.foo(1) arg = 1 >>> mod.Foo() <mod.Foo object at 0x10bf421d0> >>> s NameError: name 's' is not defined

我們可以發(fā)現(xiàn),import mod不會使調用者直接訪問到mod.py模中塊內容,只是將<module_name>放在調用者的**命名空間(namespace)**中;而在模塊(mode.py)中定義的對象則保留在模塊的命名空間中。因此,從調用者那里,只有通過點表示法 (dot notation) 以<module_name>作為前綴,才能訪問模塊中的對象。當然,我們可以通過from <module_name> import *的方式,將模塊中定義的對象導入到當前調用者的命名空間中(以下劃線_開頭的對象除外):

>>> from mod import * >>> dir() ['Foo', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'foo', 's'] >>> s 'If Comrade Napoleon says it, it must be right.'

import關鍵字是導入模塊最常見的方式,該語句主要包括兩個操作:

  • 搜索給定模塊,通過函數(shù)__import__(name, globals=None, locals=None, fromlist=(), level=0)實現(xiàn);

    該函數(shù)默認被import語句調用,可以通過覆蓋builtins.__import__模塊來改變導入的語義(不推薦這么做,也不推薦用戶使用該函數(shù),建議用戶使用importlib.import_module)。

    • name 要導入的模塊;
    • globals,字典類型,全局變量,用于確定如何解釋包上下文中的名稱;locals,忽略該參數(shù);
    • fromlist,表示應該從name模塊中導入的對象或子模塊的名稱;
    • level,表征使用絕對導入或相對導入,0表示決定導入,>0表示相對導入,數(shù)值指示相對導入的層級;

    比如,import spam,將會調用spam = __import__('spam', globals(), locals(), [], 0);import spam.ham,則調用spam = __import__('spam.ham', globals(), locals(), [], 0); from spam.ham import eggs, sausage as saus,則調用:

    _temp = __import__('spam.ham', globals(), locals(), ['eggs', 'sausage'], 0) eggs = _temp.eggs saus = _temp.sausage

    因此,當name 變量的形式為 package.module 時,通常將會返回最高層級的包(第一個點號之前的名稱),而不是以name命名的模塊。 但是,當給出了fromlist非空時,則將返回以name命名的模塊。

  • 將搜索結果綁定到一個局部作用域(import語句來實現(xiàn))。即__import__()函數(shù)搜索并創(chuàng)建模塊,然后其返回值被用來實現(xiàn)import的name binding操作

  • 導入模塊時,python首先會檢查模塊緩存sys.modules是否已經導入該模塊,若存在則直接返回;否則Python首先會搜索該模塊,如果找到該模塊,它將創(chuàng)建一個模塊對象(types.ModuleType),對其進行初始化。如果找不到命名的模塊,則會引發(fā)ModuleNotFoundError。

    import語句本身的使用就是這么簡單,但是本節(jié)中提到的幾個概念,如package, module, namespace,還需我們有一個更深入的理解。

    先導概念

    namespace & scope

    Namespaces are one honking great idea—let’s do more of those!

    — The Zen of Python, by Tim Peters

    如大佬所言,命名空間(namespace, a mapping from names to objects) 是非常極其十分偉大的想法。不同的namespace有不同的生命周期,當Python執(zhí)行程序時,它會根據(jù)需要創(chuàng)建namespace,并在不再需要時刪除它們。通常,在任何給定的時間都會存在許多命名空間,在python中主要包括三大類的namespace:

  • Built-in,可通過dir(__builtins__)查看;
  • Global,全局命名空間是指包含在主程序層定義的任何對象。Python在主程序啟動時創(chuàng)建全局命名空間,并一直存在,直到解釋器終止。嚴格地說,這可能不是唯一存在的全局命名空間。python解釋器還為程序用import語句加載的任何模塊創(chuàng)建全局命名空間;
  • Local,函數(shù)中定義的局部變量,函數(shù)參數(shù)等;
  • 我們可以分別通過dir(__builtins__) or __builtins__.__dict__, globals()以及l(fā)ocals() 查看內置、當前全局及局部命名空間中的對象:

    >>> def func(x): ... a = 2 ... print(locals()) ... >>> func(5) {'x': 5, 'a': 2} >>> >>> globals() {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'func': <function func at 0x2b256f36ae50>} >>> dir(__builtins__) ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

    接著,我們會想,當在一個程序的不同命名空間中存在相同的對象名時,程序又如何知道你想要調用一個呢?這時則需要引入一個新的概念:作用域(scope),官方定義: a textual region of a Python program where a namespace is directly accessible,即指一個 Python 程序中可以直接訪問(Directly accessible)命名空間的文字區(qū)域。Directly accessible是指嘗試在命名空間中去尋找一個unqualified reference的對象名所對應的對象。python作用域搜索遵循LEGB rule ,即假如在程序中引用x,解釋器將依次在以下作用域搜索該對象:

  • Local: 如果在函數(shù)中引用x,那么解釋器首先在該函數(shù)的最內部作用域中搜索它;
  • Enclosing: 如果x不在local作用域中,但出現(xiàn)在另一個函數(shù)內部的函數(shù)中,則解釋器將在enclosing function(請參考博客)的作用域中進行搜索;
  • Global: 如果以上兩個搜索都沒有結果,那么解釋器接下來就會在全局作用域中查找;
  • Built-in: 如果依然沒找到,最后解釋器會嘗試在內置作用域查找;
  • 如果上述四個作用域中都沒找到x,則會報錯。舉例如下:

    a = 1def func():a = 3def inner_func():a = 5print(a)inner_func()print(a)if __name__ == '__main__':func()print(a) ### output # 5 # 3 # 1

    我們再看一個有趣的例子:

    _list: list = [1, 2, 3] _int: int = 4def func(seq: list, integer: int):seq[0] = -1integer += 1if __name__ == '__main__':func(_list, _int)print(_list, _int)

    在該例子中,將全局定義的_list, _int傳入函數(shù)func, 調用執(zhí)行之后發(fā)現(xiàn),_lis改變了而_int卻沒有發(fā)生改變,這是為什么呢?學過C語言的同學應該馬上會想到這會不會是傳值和傳址的區(qū)別。很遺憾,嚴格來講都不是。因為在python中一切皆為對象,也就是說一個引用指向一個對象,而不是指向一個特定的內存地址。在python中這和可變對象 mutable不可變對象 immutable有關:

    • 當函數(shù)參數(shù)傳入不可變對象時,類似于C語言傳值(pass-by-value),在函數(shù)中的修改不會影響全局空間的對象引用;
    • 當函數(shù)參數(shù)傳入可變對象時,有一點類似于C語言傳址(pass-by-reference),在函數(shù)中的修改不會影響全局空間的對象引用,但可以修改全局空間對象中的項(item);

    那么如果我想要改變全局空間中的引用呢?這時則可以通過global和nonlocal來"修改"對象的作用域:

    a = 3def func():global aa += ab = 5def inner_func():nonlocal bb += binner_func()print(f'b={b}')if __name__ == '__main__':func()print(f'a={a}') ### output b=10 a=6

    所以,global聲明允許函數(shù)在全局作用域中訪問和修改對象;nonlocal聲明允許在enclosed function中修改enclosing function中的對象。

    Module & Packages

    模塊化編程(modular programming),是強調將計算機程序的功能分離成獨立的、可相互改變的“模塊”(module)的軟件設計技術,它使得每個模塊都包含著執(zhí)行預期功能的一個唯一方面(aspect)所必需的所有東西。python使用 函數(shù)(Functions)模塊(modules)包(packages)等概念來實現(xiàn)模塊化編程,使得代碼更具有可維護性和可重用性。在理解python import machinery之前,讀者有必要先了解modulepackages兩個概念。

    module

    我們常見的*.py文件,以及*.pyc、*.pyo、*.pyd、*.pyw等單個文件都是module。 在Python中常有三種不同的方式定義module:

  • 模塊可以用Python本身編寫,如 test.py;
  • 模塊可以用C語言編寫,并在運行時動態(tài)加載,如python標準庫re(推薦閱讀,python re 正則表達式);
  • 內置模塊則內置在解釋器中,如itertools(推薦閱讀,python函數(shù)式編程之functools、itertools、operator詳解);
  • In [17]: import re, itertools In [18]: itertools Out[18]: <module 'itertools' (built-in)>In [19]: re Out[19]: <module 're' from '/Users/jeffery/workspace/envs/common/lib/python3.7/re.py'>

    在這三種情況下,訪問模塊中的內容都是通過import語句實現(xiàn),比如我們編寫一個模塊mod.py:

    s = "If Comrade Napoleon says it, it must be right." a = [100, 200, 300]def foo(arg):print(f'arg = {arg}')class Foo:pass

    假設mod.py在一個合適的位置,這些對象可以通過如下方式導入:

    >>> import mod >>> print(mod.s) If Comrade Napoleon says it, it must be right. >>> mod.a [100, 200, 300] >>> mod.foo(['quux', 'corge', 'grault']) arg = ['quux', 'corge', 'grault'] >>> x = mod.Foo() >>> x <mod.Foo object at 0x03C181F0> >>> dir(mod) ['Foo', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'foo', 's']

    所以,所謂合適的位置是指什么呢?這就涉及到python模塊導入的路徑搜索問題,python模塊導入時,將依次搜索以下路徑:

  • 運行輸入腳本的目錄或當前目錄;
  • 環(huán)境變量PYTHONPATH中所包含的目錄列表;
  • 安裝Python時配置的與安裝相關的目錄列表;
  • 這些目錄以列表的形式存儲在sys.path中,如:

    >>> import sys >>> sys.path ['', '/Users/jeffery/workspace/envs/common/lib/python37.zip', '/Users/jeffery/workspace/envs/common/lib/python3.7', '/Users/jeffery/workspace/envs/common/lib/python3.7/lib-dynload', '/Users/jeffery/workspace/envs/common/lib/python3.7/site-packages']

    packages

    A python module which can contain submodules or recursively, subpackages. Technically, a package is a python module with an __path__ attribute. From python glossary

    先舉例解釋一下上面這句話吧,下面實例中我們可以發(fā)現(xiàn),python標準庫re不是一個package,但是是一個module;而numpy則是一個package(它包含很多modules;另外,在實際使用中作為用戶并不需要關注導入的library是module還是package)。可以將package視為文件系統(tǒng)上的一個目錄,將module視為目錄中的文件,但也不要過于字面地理解這個類比,因為包和模塊不一定需要源自文件系統(tǒng)。

    >>> import re >>> re.__path__ Traceback (most recent call last):File "<stdin>", line 1, in <module> AttributeError: module 're' has no attribute '__path__' >>> import numpy >>> numpy.__path__ ['/Users/jeffery/workspace/envs/DLBCL/lib/python3.7/site-packages/numpy']

    從概念上來講,package又包括regular package和namespace package PEP420:

    regular package

    Regular package,即常規(guī)包,一般是指每一個文件夾或子文件夾下都包含__init__.py(模塊初始化文件,可以為空)。默認情況下,submodules 和 subpackages不會被導入,而通過__init__.py可以將任意或全部子模塊按照你的想法進行導入。比如在我寫的一個示例packagepython_dotplot中,可以通過__init__.py的__all__默認暴露且只暴露出部分你想暴露的接口。

    namespace package

    Namespace package,即命名空間包,該概念是在python 3.3中新增加的特性,是一種將單個Python包拆分到磁盤上多個目錄的機制(當然不局限于磁盤空間)。命名空間包隱式建立,即如果你有一個包含.py文件但沒有__init__.py文件的目錄,那么該文件夾則是一個命名空間包。

    importlib

    繞了這么遠的路,終于來到今天的正題。通常,要在一個模塊中使用另一個模塊中的代碼我們可以通過import來實現(xiàn),但這不是唯一的方法,用戶可以使用其他的導入機制,如importlib.import_module(),繞過__import__()并使用自己的解決方案實現(xiàn)模塊導入。那么,python是通過怎樣的機制來實現(xiàn)模塊的導入呢?

    當導入一個特定模塊時,首先搜索模塊緩存sys.modules中是否存在需要導入的模塊,如果存在則直接返回;如果為None,則拋出ModuleNotFoundError;如果不存在,python將依次利用finders和loaders查找并加載該模塊。

    Loaders & Finders

    詳細來講,當要導入一個sys.modules中不存在的模塊時,將依次使用sys.meta_path中的finders對象去查詢他們是否知道如何導入用戶所需模塊。如果finder可以處理,則返回一個spec對象(spec對象中則包含一個loader對象及模塊導入相關的信息),反之返回None。如果sys.meta_path中的finder循環(huán)完都是返回None, 則會拋出錯誤ModuleNotFoundError。

    >>> sys.modules {'sys': <module 'sys' (built-in)>, 'builtins': <module 'builtins' (built-in)>, '_frozen_importlib': <module 'importlib._bootstrap' (frozen)>, '_imp': <module '_imp' (built-in)>, '_thread': <module '_thread' (built-in)>, '_warnings': <module '_warnings' (built-in)>, '_weakref': <module '_weakref' (built-in)>, 'zipimport': <module 'zipimport' (built-in)>, '_frozen_importlib_external': <module 'importlib._bootstrap_external' (frozen)>, '_io': <module 'io' (built-in)>, 'marshal': <module 'marshal' (built-in)>, 'posix': <module 'posix' (built-in)>}
  • Finder: 它的工作是確定它是否可以使用它所知道的任何策略找到命名模塊,內置實現(xiàn)的finder和importers包括以下三個:

    >>> sys.meta_path [<class '_frozen_importlib.BuiltinImporter'>, # 定位內置模塊<class '_frozen_importlib.FrozenImporter'>, # 定位frozen模塊<class '_frozen_importlib_external.PathFinder'> # 定位指定path(sys.path)中的模塊,又被稱為 Path Based Finder]

    python 3.4 更新: 在python 3.4之前, finders 直接返回 loaders;而python 3.4及以后finders返回module specs,module spec則包含了 loaders。 Loaders在導入過程中依然用到,但承擔更少的職能了。

  • Loader:loaders提供模塊加載過程中最重要的一個功能:module execution,導入機制調用exec_module(module)執(zhí)行模塊代碼,該函數(shù)的一切返回值都忽略。Loader必須實現(xiàn)以下兩個條件:

    • 如果load一個python模塊,loader必須在module的全局命名空間執(zhí)行模塊代碼(module.__dict__);
    • 如果Loader不能執(zhí)行該模塊,則應拋出ImportError;

    Loaders可以選擇在加載期間通過實現(xiàn) create_module(module_spec) 方法來創(chuàng)建module對象,在模塊創(chuàng)建之后,但在執(zhí)行之前,導入機制會初始化設置與導入相關的模塊信息(基于ModuleSpec對象的信息),主要包括以下屬性:

    __name__ # fully-qualified name of the module __loader__ # load模塊時所對應的loader對象 __package__ # 和__spec__.parent的值一樣 __spec__ # 導入模塊時所對應的module spec對象 __path__ # package的一個必要屬性,數(shù)據(jù)類型為iterable[strings],該屬性在導入子模塊時會用到 __file__ # package 所在路徑 __cached__ # 代碼編譯版本的路徑

    Importers: 同時實現(xiàn)了finders和loaders的對象

  • import hooks

    python的導入機制是可擴展的,主要通過Import hooks(meta hooks and import path hooks)的機制來實現(xiàn):

  • meta hooks: 在import開始時meta hook會被調用,具體是在sys.modules緩存查找之后,任何其他導入操作發(fā)生之前。 這允許meta hook覆蓋sys.path 、frozen模塊,甚至built-in模塊。meta hook是通過向 sys.meta_path 添加新的 finder 對象來注冊的;

    >>> sys.meta_path[<class '_frozen_importlib.BuiltinImporter'>, # 定位內置模塊<class '_frozen_importlib.FrozenImporter'>, # 定位frozen模塊<class '_frozen_importlib_external.PathFinder'> # 定位指定path(sys.path)中的模塊]

    ? 上述的meta path finders/importers都實現(xiàn)了find_spec(fullname, path, target=None)函數(shù),第一個參數(shù)是導入模塊的fully qualified name;當導入頂層模塊(top-level module)時第二個參數(shù)為None,反之為模塊上一級的__path__屬性(參考例子),第三個參數(shù)為一個已經存在的作為導入模塊目標的module object (當且僅當調用importlib.reload()時使用該參數(shù))。其中,第三個finder又被稱為Path Based Finder,它通過搜索sys.path中指定的路徑(path entry)來搜索模塊。

    ? 與前兩個不同的是,Path Based Finder本身并沒有實現(xiàn)導入模塊的操作;相反,它遍歷搜索路徑(sys.path)中的各個path entry,并關聯(lián)知道如何處理該path entry的path entry finders。Path Based Finder主要利用sys.path, sys.path_hooks, sys.path_importer_cache 以及package.__path__幾個變量。

    >>> sys.path ['/Applications/PyCharm.app/Contents/helpers/pydev', '/Applications/PyCharm.app/Contents/helpers/pycharm_display', '/Applications/PyCharm.app/Contents/helpers/third_party/thriftpy', '/Applications/PyCharm.app/Contents/helpers/pydev', '/Users/jeffery/workspace/envs/test/lib/python37.zip', '/Users/jeffery/workspace/envs/test/lib/python3.7', '/Users/jeffery/workspace/envs/test/lib/python3.7/lib-dynload', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7', '/Users/jeffery/workspace/envs/test/lib/python3.7/site-packages', '/Applications/PyCharm.app/Contents/helpers/pycharm_matplotlib_backend', '/Users/jeffery/PycharmProjects/python-dotplot']

    ? 所以,Path Based Finder通過調用find_spec(path_entry_related_path)函數(shù)尋找指定模塊并關聯(lián)相對應的path entry finders。為了方便,Path Based Finder維護了一個緩存字典,表征每個path entry應該由哪個path entry finder處理,如下所示:

    >>> sys.path_importer_cache # path entry finders {'/Applications/PyCharm.app/Contents/helpers/pycharm_matplotlib_backend': FileFinder('/Applications/PyCharm.app/Contents/helpers/pycharm_matplotlib_backend')}
  • import path hooks: import path hooks作為 sys.path(或 package.__path__)處理的一部分,其通過向 sys.path_hooks 添加新的可調用對象來注冊的

  • 如果緩存字典中不存在該path entry,Path Based Finder將path entry作為參數(shù)循環(huán)調用sys.path_hooks中的每一個callable對象,返回一個可以處理該path entry的path entry finder或者拋出錯誤ImportError。如果循環(huán)結束都沒有找到合適的path entry finder,find_spec函數(shù)將會將緩存mapping中path entry的值設置為None并返回None,代表該meta path finder不能找到該module。如果sys.path_hooks中的某個hook返回了一個path entry finder,則該path entry finder用于后續(xù)module spec對象的構建及模塊加載。path entry finder必須實現(xiàn)find spec方法,返回module spec對象,該方法包含兩個參數(shù):模塊的fully qualified name以及target module (optional)。

    >>> sys.path_hooks >>> [zipimport.zipimporter,<function _frozen_importlib_external.FileFinder.path_hook.<locals>.path_hook_for_FileFinder(path)>]

    importlib.abc

    importlib.abc模塊包含import關鍵字用到的所有核心抽象基類,主要包含以下這些類:

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

    其中,imporlib.machinery模塊利用importlib.abc模塊實現(xiàn)了很多用于尋找、加載模塊的功能。

    • BuiltinImporter

      BuiltinImporter是一個針對built-in模塊的importer, 即sys.builtin_module_names中所包含的所有模塊。該類實現(xiàn)了importlib.abc.MetaPathFinder 和importlib.abc.InspectLoader的抽象。

    • FrozenImporter

      FrozenImporter是一個針對frozen模塊的importer。該類也實現(xiàn)了importlib.abc.MetaPathFinder 和importlib.abc.InspectLoader相關的功能。

    • PathFinder

      PathFinder是一個針對sys.path 和package __path__ 屬性的Finder,該類實現(xiàn)了importlib.abc.MetaPathFinder 的抽象。

    • FileFinder

      importlib.abc.PathEntryFinder 的具體實現(xiàn),用來緩存來自文件系統(tǒng)的結果。

    importlib.resources

    If you can import a package, you can access resources within that package.

    importlib.resources 模塊是在 python 3.7新增的功能模塊,其允許像導入模塊一樣導入包內的資源(resources)。 資源可以是位于可導入包中的任何文件,該文件可能是文件系統(tǒng)上的物理文件也可能是網絡資源。但importlib.resources 只支持regular package,不支持namespace package。

    該模塊首先定義了以下兩種數(shù)據(jù)類型,作為主要的兩種參數(shù)實現(xiàn)該模塊功能:

    Package = Union[str, ModuleType] # 包,如果Package是ModuleType, 那么其該屬性__spec__.submodule_search_locations不可為None Resource = Union[str, os.PathLike] # 資源名稱

    我們簡單的構建如下層級結構的regular package作為示例,來看該模塊主要的函數(shù):

    test_package ├── __init__.py ├── data │ ├── __init__.py │ └── data.txt └── data0.txt

    其中,test_package/_init_.py文件包含以下代碼:

    from .data import *print('____root init____')

    test_package/data/_init_.py文件中包含以下代碼:

    print('____data dir init____')

    data0.txt文件包含一行數(shù)據(jù):

    this is data0.

    data.txt文件包含兩行數(shù)據(jù):

    col1 col2 data1 data2
    • is_resource(package, name), contents(package)

      for item in contents(test_package):if is_resource(test_package, item) and item.endswith('.txt'):print(item)# Output: # data0.txt

      contents()函數(shù)返回一個可迭代對象,該可迭代對象包含package中的所有資源(如,文件)和非資源(如,文件夾),通過is_resource可以進一步判斷類型,如果是資源則返回True,反之返回False。

    • open_binary(package, resource), open_text(package, resource, encoding='utf-8', errors='strict')

      for item in contents(test_package):if is_resource(test_package, item) and item.endswith('.txt'):with open_text(test_package, item) as f:line = f.readline()print(line) # Output: # This is data0.

      進一步地,我們可以使用上述兩個函數(shù)打開數(shù)據(jù)文件從而讀取數(shù)據(jù),前者返回typing.BinaryIO, 后者返回typing.TextIO,分別代表讀狀態(tài)下的字節(jié)/文本 I/O stream。

    • read_binary(package, resource), read_text(package, resource, encoding='utf-8', errors='strict')

      也可以使用更直接的方式,打開并讀取數(shù)據(jù)文件。這里值得注意的是,我們讀取的目標是子模塊data中的數(shù)據(jù)文件,可以通過傳入?yún)?shù)package.submodule來實現(xiàn),也就是說contents()函數(shù)不會去遍歷子文件夾的內容。

      for item in contents(test_package.data):if is_resource(test_package.data, item) and item.endswith('.txt'):print(read_binary(test_package.data, item))# Output: # b'col1 col2\ndata1 data2'
    • path(package, resource) 該函數(shù)可以返回一個ContextManager對象,指示資源的路徑

      with path(test_package, 'data0.txt') as file: # ContextManagerprint(file, type(file))# Output: # /path/to/test_package/data0.txt <class 'pathlib.PosixPath'>
    • files(package), as_file(Traversable)

      這兩個方法是在python 3.9中新增的,前者返回一個importlib.resources.abc.Traversable,后者參數(shù)一般為前者返回值

      test_traversable = files(test_package) print(test_traversable, isinstance(test_traversable, resources_abc.Traversable)) with as_file(test_traversable) as traversable_files:print(traversable_files, type(traversable_files)) # Output /path/to/test_package True /path/to/test_package <class 'pathlib.PosixPath'>

    ##Importlib 常用API

    • importlib.import_module(name, package=None)

    name參數(shù)指定導入模塊名字,以絕對(absolute)或則相對(relative)的方式導入模塊;當name是relative形式(如..mod),則需要指定package參數(shù)作為一個anchor來解析包的名字或路徑(如pkg.subpkg)。所以importlib.import_module('..mod', 'pkg.subpkg')將會導入模塊pkg.mod。實際上,該函數(shù)是基于importlib.__import__(name, globals=None, locals=None, fromlist=(), level=0)來實現(xiàn)的,區(qū)別是,前者返回用戶指定的包或模塊(如pkg.mod),后者返回的是top-level 的包或模塊。為了更深入了解python導入包的過程我們可以結合源碼來分析一下整個過程。

    • import_module(name, package)根據(jù)參數(shù)形式確定是相對導入還是絕對導入,并調用importlib包中的_gcd_import(name, package, level)函數(shù)

      def import_module(name, package=None):"""Import a module.當相對導入的時候,才需要用到package參數(shù),作為導入模式時的anchor point."""level = 0if name.startswith('.'):if not package:msg = ("the 'package' argument is required to perform a relative ""import for {!r}")raise TypeError(msg.format(name))for character in name: # 當 name='..data', level=2; 當name='...data', level=3if character != '.':breaklevel += 1return _bootstrap._gcd_import(name[level:], package, level)
    • _gcd_import(name, package, level)函數(shù)進一步調用importlib中的_find_and_load(name, import_)函數(shù)。值得注意的是,gcd,即greatest common denominator,也即最大公約數(shù)。為什么起這么個名字呢?因為該函數(shù)代表了builtin __import__函數(shù)和importlib.import_module函數(shù)功能的主要相似之處。

      This function represents the greatest common denominator of functionality between import_module and __import__. This includes setting __package__ if the loader did not.

      def _gcd_import(name, package=None, level=0):"""返回name,package所指定的module"""_sanity_check(name, package, level)if level > 0:name = _resolve_name(name, package, level) # _resolve_name將得到要導入模塊的全路徑,如numpy, numpy.random等return _find_and_load(name, _gcd_import)
    • 從返回值可見,_find_and_load(name, import_)函數(shù)實現(xiàn)了搜索加載的功能,最后返回指定的模塊。在搜索開始前,先查詢模塊緩存sys.modules中是否已經存在要導入的指定模塊;然后搜索加載的功能實際上由_find_and_load_unlocked(name, import_)承擔。

      def _find_and_load(name, import_):"""Find and load the module."""with _ModuleLockManager(name):module = sys.modules.get(name, _NEEDS_LOADING)if module is _NEEDS_LOADING:return _find_and_load_unlocked(name, import_)if module is None:message = ('import of {} halted; ''None in sys.modules'.format(name))raise ModuleNotFoundError(message, name=name)_lock_unlock_module(name) # 用于確保模塊被完全初始化,以防它被另一個線程導入。return module
    • 從源碼我們可以非常清晰的看出,主要是利用_find_spec(name, path, target=None)獲得模塊/包的ModuleSpec對象;然后再利用_load_unlocked(spec)函數(shù)根據(jù)ModuleSpec對象信息獲得module對象。可以預見,以下再分別分析這兩個過程就可以知道全部過程啦。

      def _find_and_load_unlocked(name, import_):path = Noneparent = name.rpartition('.')[0]if parent:if parent not in sys.modules:_call_with_frames_removed(import_, parent) # 如果存在父級,則先導入父級;# Crazy side-effects!if name in sys.modules:return sys.modules[name]parent_module = sys.modules[parent]try:path = parent_module.__path__except AttributeError:msg = (_ERR_MSG + '; {!r} is not a package').format(name, parent)raise ModuleNotFoundError(msg, name=name) from Nonespec = _find_spec(name, path)if spec is None:raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)else:module = _load_unlocked(spec)if parent:# Set the module as an attribute on its parent.parent_module = sys.modules[parent]child = name.rpartition('.')[2]try:setattr(parent_module, child, module)except AttributeError:msg = f"Cannot set an attribute on {parent!r} for child module {child!r}"_warnings.warn(msg, ImportWarning)return module
    • 為了獲得模塊/包的ModuleSpec對象,_find_spec(name, path, target=None)函數(shù)循環(huán)調用sys.meta_path中的importers/Finders嘗試能不能處理該指定的模塊,如果可以則返回ModuleSpec對象,反之返回None。

      def _find_spec(name, path, target=None):"""Find a module's spec."""meta_path = sys.meta_pathif meta_path is None:# PyImport_Cleanup() is running or has been called.raise ImportError("sys.meta_path is None, Python is likely ""shutting down")if not meta_path:_warnings.warn('sys.meta_path is empty', ImportWarning)# We check sys.modules here for the reload case. While a passed-in# target will usually indicate a reload there is no guarantee, whereas# sys.modules provides one.is_reload = name in sys.modulesfor finder in meta_path:with _ImportLockContext():try:find_spec = finder.find_specexcept AttributeError:spec = _find_spec_legacy(finder, name, path)if spec is None:continueelse:spec = find_spec(name, path, target)if spec is not None:# The parent import may have already imported this module.if not is_reload and name in sys.modules:module = sys.modules[name]try:__spec__ = module.__spec__except AttributeError:# We use the found spec since that is the one that# we would have used if the parent module hadn't# beaten us to the punch.return specelse:if __spec__ is None:return specelse:return __spec__else:return specelse:return None
    • 獲得了模塊/包的ModuleSpec對象之后,再調用_load_unlocked(spec)函數(shù)加載并執(zhí)行模塊。

      def _load_unlocked(spec):# A helper for direct use by the import system.if spec.loader is not None:# Not a namespace package.if not hasattr(spec.loader, 'exec_module'):return _load_backward_compatible(spec)module = module_from_spec(spec) # 利用importlib.module_from_spec獲得模塊spec._initializing = Truetry:sys.modules[spec.name] = module # 執(zhí)行模塊前,應該先將模塊放入模塊緩存中try:if spec.loader is None:if spec.submodule_search_locations is None:raise ImportError('missing loader', name=spec.name)# A namespace package so do nothing.else:spec.loader.exec_module(module) # 執(zhí)行模塊代碼except:try:del sys.modules[spec.name]except KeyError:passraisemodule = sys.modules.pop(spec.name)sys.modules[spec.name] = module_verbose_message('import {!r} # {!r}', spec.name, spec.loader)finally:spec._initializing = Falsereturn module

    因此,我們可以總結該函數(shù)的近似實現(xiàn)如下:

    import importlib.util import sysdef import_module(name, package=None):"""An approximate implementation of import."""absolute_name = importlib.util.resolve_name(name, package)try:return sys.modules[absolute_name]except KeyError:passpath = Noneif '.' in absolute_name: # 例如為numpy.random時,先import numpy,再回來import子模塊randomparent_name, _, child_name = absolute_name.rpartition('.')parent_module = import_module(parent_name)path = parent_module.__spec__.submodule_search_locationsfor finder in sys.meta_path:spec = finder.find_spec(absolute_name, path) # 根據(jù)submodule_search_locations搜索子模塊if spec is not None:breakelse:msg = f'No module named {absolute_name!r}'raise ModuleNotFoundError(msg, name=absolute_name)module = importlib.util.module_from_spec(spec)spec.loader.exec_module(module) # 這一步很重要sys.modules[absolute_name] = module # optional,推薦加上if path is not None:setattr(parent_module, child_name, module)return module
    • importlib.invalidate_caches()

      該函數(shù)使sys.meta_path中緩存的Finders失效,適用于動態(tài)導入程序執(zhí)行之后才創(chuàng)建/安裝的包的場景。

    • importlib.reload(module)

      重新導入一個已導入的模塊(module),以上面的test_package為例,并修改test_package/_init_.py 如下:

      from .data import *print('____root init____') num = 10

      進入python console 執(zhí)行下列代碼:

      >>> import test_package ____data dir init____ ____root init____ >>> test_package.num 10

      此時,不要關閉console,并直接修改源碼,將num=10這一行改為num=11,此時重新導入test_package,我們會發(fā)現(xiàn)num的值并不會改變:

      >>> import test_package >>> test_package.num 10

      這種情況下則需要用到reload函數(shù)來使源碼的修改得到應用:

      >>> import importlib >>> importlib.reload(test_package) >>> test_package = importlib.reload(test_package) ____root init____ >>> test_package.num 11
    • importlib.util.find_spec()

      通過importlib.util.find_spec()測試一個模塊是否可以被導入,如果可以被導入則使用importlib.util.module_from_spec獲得模塊對象并執(zhí)行module code, 執(zhí)行l(wèi)oad操作導入模塊。

      import importlib.util import sysname = 'itertools' module = Nonespec = importlib.util.find_spec(name) if spec is None:print("can't find the itertools module") else:# 實際導入操作module = importlib.util.module_from_spec(spec)spec.loader.exec_module(module)sys.modules[name] = module # optionalprint(list(module.accumulate([1, 2, 3])))

      該函數(shù)是python3.4的新增函數(shù),用來替代python3.3版本中的importlib.find_loader(name, path=None)

    • importlib.util.spec_from_file_location

      也可以通過importlib.util.spec_from_file_location導入python源文件

      import importlib.util import sys# For illustrative purposes. import tokenize file_path = tokenize.__file__ module_name = tokenize.__name__spec = importlib.util.spec_from_file_location(module_name, file_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # Optional; only necessary if you want to be able to import the module # by name later. sys.modules[module_name] = module
    • importlib.util.spec_from_loader

      基于Loader創(chuàng)建 ModuleSpec對象

    • importlib.util.module_from_spec

      通過importlib.util.find_spec()測試一個模塊是否可以被導入,如果可以被導入則使用importlib.util.module_from_spec獲得模塊對象并執(zhí)行module code, 執(zhí)行l(wèi)oad操作導入模塊。

    • importlib.machinery.ModuleSpec(name, loader, *, origin=None, loader_state=None, is_package=None)

      ModuleSpec是一個模塊的import-system-related 狀態(tài),對于一個模塊,可以通過module.__spec__來獲取相關的信息。該類主要包括以下重要的屬性(注意,以下屬性說明中,圓括弧內的屬性名可以直接通過模塊來訪問,module.__spec__.origin == module.__file__):

      • name (__name__) 模塊的名字(fully-qualified name)

        In [1]: import numpy.randomIn [2]: from numpy import randomIn [3]: numpy.random.__name__ Out[3]: 'numpy.random'In [4]: random.__name__ Out[4]: 'numpy.random'
      • loader (__loader__) import該模塊時所使用的Loader

        In [5]: random.__loader__ Out[5]: <_frozen_importlib_external.SourceFileLoader at 0x2abdf0f8bf10>
      • origin (__file__) 模塊在文件系統(tǒng)上的位置

        In [6]: random.__file__ Out[6]: '/path/to/python/site-packages/numpy/random/__init__.py'
      • submodule_search_locations (__path__) package的子模塊搜尋路徑,如果不是package返回None

        In [7]: random.__path__ Out[7]: ['/path/to/python/site-packages/numpy/random']
      • loader_state 加載期間使用的額外模塊特定數(shù)據(jù)的容器或則None

      • cached (__cached__) compiled module的位置

        In [8]: random.__cached__ Out[8]: '/path/to/python/site-packages/numpy/random/__pycache__/__init__.cpython-38.pyc'
      • parent (__package__) fully-qualified name

        In [9]: random.__package__ Out[9]: 'numpy.random'
      • has_location bool值,指示模塊的“origin”屬性是否指代一個可加載位置

        In [10]: random.__spec__.has_location Out[10]: True

    ##importlib應用

    寫到這里,python 的導入機制其實就基本寫完了。總結來講,python導入一個包主要涉及以下三個步驟:

  • 確認要導入的模塊/包是否已經存在模塊緩存中sys.modules,如果存在,則返回,進行命名綁定即可;

  • 依次循環(huán)查詢sys.meta_path中的importers/finders,看是否能處理要導入的模塊/包(不能處理返回None;能處理則返回一個ModuleSpec對象);

    這一步可能會比較復雜,當要導入的包既不是built-in也不是frozen包時,則需要利用PathFinder查找搜索路徑sys.path找到相應的包。由于PathFinder本身沒有實現(xiàn)模塊導入功能,找到包之后需要結合sys.path_importer_cache或sys.path_hooks關聯(lián)能導入該包的path entry finder

  • 利用上一步返回的ModuleSpec對象(ModuleSpec包含Loader對象)導入包;

  • 基于此,我們就可以很好利用Finder和Loader的概念來自定義模塊/包的導入行為啦。我們可以利用修改import hooks(sys.meta_path, sys.path_hooks)的方法,改變import默認的加載方式。比如,我們可以實現(xiàn)一個包的導入機制,如果該包不存在則利用pypi安裝再導入(更多自定義導入例子,請參考導入數(shù)據(jù)文件):

    from importlib import util import subprocess import sysclass PipFinder:@classmethoddef find_spec(cls, fullname, path, target=None):print(f"Module {fullname!r} not installed. Attempting to pip install")cmd = f"{sys.executable} -m pip install {fullname}"try:subprocess.run(cmd.split(), check=True)except subprocess.CalledProcessError:return Nonereturn util.find_spec(fullname)sys.meta_path.append(PipFinder)

    這時,導入未安裝的模塊時,將會自動安裝相應模塊:

    In [1]: import parse Module 'parse' not installed. Attempting to pip install Collecting parseDownloading parse-1.19.0.tar.gz (30 kB) Building wheels for collected packages: parseBuilding wheel for parse (setup.py) ... doneCreated wheel for parse: filename=parse-1.19.0-py3-none-any.whl size=24581 sha256=90690a858905aa38e2935201a725276fa85e928cb5f6b16236b6a1494985850eStored in directory: ~/.cache/pip/wheels/d6/9c/58/ee3ba36897e890f3ad81e9b730791a153fce20caa4a8a474df Successfully built parse Installing collected packages: parse Successfully installed parse-1.19.0

    參考資料

    Python import system

    Python importing modules

    PEP451

    Stackoverflow How to use the __import__function to import a name from a submodule?

    Python Classes-namespace and scope

    Python import

    scope

    namespace

    總結

    以上是生活随笔為你收集整理的python导入机制及importlib模块的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。