python导入机制及importlib模块
文章目錄
- 寫在篇前
 - import 關鍵字
 - 先導概念
 - namespace & scope
 - Module & Packages
 - module
 - packages
 - regular package
 - namespace package
 
- importlib
 - Loaders & Finders
 - import hooks
 - importlib.abc
 - importlib.resources
 
- 參考資料
 
寫在篇前
這篇博客的雛形,嚴格來講,在我腦海中浮現已有近一年之久,起源于我之前在寫一個python模塊并用jupyter notebook測試時發現,當在一個session中通過import導入模塊,修改模塊再次通過import導入該模塊時,模塊修改并不會生效。至此,通過一番研究發現,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>, ... # 為了代碼規范,不推薦該使用方式 from <module_name> import * # 不推薦我們舉個例子, 假設我們寫了一個模塊mod.py,在該模塊中定義了一個字符串s,一個list a, 一個函數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()函數查看導入前后當前命名空間變量的變化:
>>> 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我們可以發現,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關鍵字是導入模塊最常見的方式,該語句主要包括兩個操作:
搜索給定模塊,通過函數__import__(name, globals=None, locals=None, fromlist=(), level=0)實現;
該函數默認被import語句調用,可以通過覆蓋builtins.__import__模塊來改變導入的語義(不推薦這么做,也不推薦用戶使用該函數,建議用戶使用importlib.import_module)。
- name 要導入的模塊;
 - globals,字典類型,全局變量,用于確定如何解釋包上下文中的名稱;locals,忽略該參數;
 - fromlist,表示應該從name模塊中導入的對象或子模塊的名稱;
 - level,表征使用絕對導入或相對導入,0表示決定導入,>0表示相對導入,數值指示相對導入的層級;
 
比如,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語句來實現)。即__import__()函數搜索并創建模塊,然后其返回值被用來實現import的name binding操作
導入模塊時,python首先會檢查模塊緩存sys.modules是否已經導入該模塊,若存在則直接返回;否則Python首先會搜索該模塊,如果找到該模塊,它將創建一個模塊對象(types.ModuleType),對其進行初始化。如果找不到命名的模塊,則會引發ModuleNotFoundError。
import語句本身的使用就是這么簡單,但是本節中提到的幾個概念,如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執行程序時,它會根據需要創建namespace,并在不再需要時刪除它們。通常,在任何給定的時間都會存在許多命名空間,在python中主要包括三大類的namespace:
我們可以分別通過dir(__builtins__) or __builtins__.__dict__, globals()以及locals() 查看內置、當前全局及局部命名空間中的對象:
>>> 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)命名空間的文字區域。Directly accessible是指嘗試在命名空間中去尋找一個unqualified reference的對象名所對應的對象。python作用域搜索遵循LEGB rule ,即假如在程序中引用x,解釋器將依次在以下作用域搜索該對象:
 
如果上述四個作用域中都沒找到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傳入函數func, 調用執行之后發現,_lis改變了而_int卻沒有發生改變,這是為什么呢?學過C語言的同學應該馬上會想到這會不會是傳值和傳址的區別。很遺憾,嚴格來講都不是。因為在python中一切皆為對象,也就是說一個引用指向一個對象,而不是指向一個特定的內存地址。在python中這和可變對象 mutable和不可變對象 immutable有關:
- 當函數參數傳入不可變對象時,類似于C語言傳值(pass-by-value),在函數中的修改不會影響全局空間的對象引用;
 - 當函數參數傳入可變對象時,有一點類似于C語言傳址(pass-by-reference),在函數中的修改不會影響全局空間的對象引用,但可以修改全局空間對象中的項(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聲明允許函數在全局作用域中訪問和修改對象;nonlocal聲明允許在enclosed function中修改enclosing function中的對象。
Module & Packages
模塊化編程(modular programming),是強調將計算機程序的功能分離成獨立的、可相互改變的“模塊”(module)的軟件設計技術,它使得每個模塊都包含著執行預期功能的一個唯一方面(aspect)所必需的所有東西。python使用 函數(Functions),模塊(modules)和包(packages)等概念來實現模塊化編程,使得代碼更具有可維護性和可重用性。在理解python import machinery之前,讀者有必要先了解module和packages兩個概念。
module
我們常見的*.py文件,以及*.pyc、*.pyo、*.pyd、*.pyw等單個文件都是module。 在Python中常有三種不同的方式定義module:
在這三種情況下,訪問模塊中的內容都是通過import語句實現,比如我們編寫一個模塊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模塊導入時,將依次搜索以下路徑:
這些目錄以列表的形式存儲在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
先舉例解釋一下上面這句話吧,下面實例中我們可以發現,python標準庫re不是一個package,但是是一個module;而numpy則是一個package(它包含很多modules;另外,在實際使用中作為用戶并不需要關注導入的library是module還是package)。可以將package視為文件系統上的一個目錄,將module視為目錄中的文件,但也不要過于字面地理解這個類比,因為包和模塊不一定需要源自文件系統。
>>> 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,即常規包,一般是指每一個文件夾或子文件夾下都包含__init__.py(模塊初始化文件,可以為空)。默認情況下,submodules 和 subpackages不會被導入,而通過__init__.py可以將任意或全部子模塊按照你的想法進行導入。比如在我寫的一個示例packagepython_dotplot中,可以通過__init__.py的__all__默認暴露且只暴露出部分你想暴露的接口。
namespace package
Namespace package,即命名空間包,該概念是在python 3.3中新增加的特性,是一種將單個Python包拆分到磁盤上多個目錄的機制(當然不局限于磁盤空間)。命名空間包隱式建立,即如果你有一個包含.py文件但沒有__init__.py文件的目錄,那么該文件夾則是一個命名空間包。
importlib
繞了這么遠的路,終于來到今天的正題。通常,要在一個模塊中使用另一個模塊中的代碼我們可以通過import來實現,但這不是唯一的方法,用戶可以使用其他的導入機制,如importlib.import_module(),繞過__import__()并使用自己的解決方案實現模塊導入。那么,python是通過怎樣的機制來實現模塊的導入呢?
當導入一個特定模塊時,首先搜索模塊緩存sys.modules中是否存在需要導入的模塊,如果存在則直接返回;如果為None,則拋出ModuleNotFoundError;如果不存在,python將依次利用finders和loaders查找并加載該模塊。
Loaders & Finders
詳細來講,當要導入一個sys.modules中不存在的模塊時,將依次使用sys.meta_path中的finders對象去查詢他們是否知道如何導入用戶所需模塊。如果finder可以處理,則返回一個spec對象(spec對象中則包含一個loader對象及模塊導入相關的信息),反之返回None。如果sys.meta_path中的finder循環完都是返回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: 它的工作是確定它是否可以使用它所知道的任何策略找到命名模塊,內置實現的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)執行模塊代碼,該函數的一切返回值都忽略。Loader必須實現以下兩個條件:
- 如果load一個python模塊,loader必須在module的全局命名空間執行模塊代碼(module.__dict__);
 - 如果Loader不能執行該模塊,則應拋出ImportError;
 
Loaders可以選擇在加載期間通過實現 create_module(module_spec) 方法來創建module對象,在模塊創建之后,但在執行之前,導入機制會初始化設置與導入相關的模塊信息(基于ModuleSpec對象的信息),主要包括以下屬性:
__name__ # fully-qualified name of the module __loader__ # load模塊時所對應的loader對象 __package__ # 和__spec__.parent的值一樣 __spec__ # 導入模塊時所對應的module spec對象 __path__ # package的一個必要屬性,數據類型為iterable[strings],該屬性在導入子模塊時會用到 __file__ # package 所在路徑 __cached__ # 代碼編譯版本的路徑Importers: 同時實現了finders和loaders的對象
import hooks
python的導入機制是可擴展的,主要通過Import hooks(meta hooks and import path hooks)的機制來實現:
meta hooks: 在import開始時meta hook會被調用,具體是在sys.modules緩存查找之后,任何其他導入操作發生之前。 這允許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都實現了find_spec(fullname, path, target=None)函數,第一個參數是導入模塊的fully qualified name;當導入頂層模塊(top-level module)時第二個參數為None,反之為模塊上一級的__path__屬性(參考例子),第三個參數為一個已經存在的作為導入模塊目標的module object (當且僅當調用importlib.reload()時使用該參數)。其中,第三個finder又被稱為Path Based Finder,它通過搜索sys.path中指定的路徑(path entry)來搜索模塊。
? 與前兩個不同的是,Path Based Finder本身并沒有實現導入模塊的操作;相反,它遍歷搜索路徑(sys.path)中的各個path entry,并關聯知道如何處理該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)函數尋找指定模塊并關聯相對應的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作為參數循環調用sys.path_hooks中的每一個callable對象,返回一個可以處理該path entry的path entry finder或者拋出錯誤ImportError。如果循環結束都沒有找到合適的path entry finder,find_spec函數將會將緩存mapping中path entry的值設置為None并返回None,代表該meta path finder不能找到該module。如果sys.path_hooks中的某個hook返回了一個path entry finder,則該path entry finder用于后續module spec對象的構建及模塊加載。path entry finder必須實現find spec方法,返回module spec對象,該方法包含兩個參數:模塊的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模塊實現了很多用于尋找、加載模塊的功能。
-  
BuiltinImporter
BuiltinImporter是一個針對built-in模塊的importer, 即sys.builtin_module_names中所包含的所有模塊。該類實現了importlib.abc.MetaPathFinder 和importlib.abc.InspectLoader的抽象。
 -  
FrozenImporter
FrozenImporter是一個針對frozen模塊的importer。該類也實現了importlib.abc.MetaPathFinder 和importlib.abc.InspectLoader相關的功能。
 -  
PathFinder
PathFinder是一個針對sys.path 和package __path__ 屬性的Finder,該類實現了importlib.abc.MetaPathFinder 的抽象。
 -  
FileFinder
importlib.abc.PathEntryFinder 的具體實現,用來緩存來自文件系統的結果。
 
importlib.resources
If you can import a package, you can access resources within that package.
importlib.resources 模塊是在 python 3.7新增的功能模塊,其允許像導入模塊一樣導入包內的資源(resources)。 資源可以是位于可導入包中的任何文件,該文件可能是文件系統上的物理文件也可能是網絡資源。但importlib.resources 只支持regular package,不支持namespace package。
該模塊首先定義了以下兩種數據類型,作為主要的兩種參數實現該模塊功能:
Package = Union[str, ModuleType] # 包,如果Package是ModuleType, 那么其該屬性__spec__.submodule_search_locations不可為None Resource = Union[str, os.PathLike] # 資源名稱我們簡單的構建如下層級結構的regular package作為示例,來看該模塊主要的函數:
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文件包含一行數據:
this is data0.data.txt文件包含兩行數據:
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.txtcontents()函數返回一個可迭代對象,該可迭代對象包含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.進一步地,我們可以使用上述兩個函數打開數據文件從而讀取數據,前者返回typing.BinaryIO, 后者返回typing.TextIO,分別代表讀狀態下的字節/文本 I/O stream。
 -  
read_binary(package, resource), read_text(package, resource, encoding='utf-8', errors='strict')
也可以使用更直接的方式,打開并讀取數據文件。這里值得注意的是,我們讀取的目標是子模塊data中的數據文件,可以通過傳入參數package.submodule來實現,也就是說contents()函數不會去遍歷子文件夾的內容。
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) 該函數可以返回一個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,后者參數一般為前者返回值
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參數指定導入模塊名字,以絕對(absolute)或則相對(relative)的方式導入模塊;當name是relative形式(如..mod),則需要指定package參數作為一個anchor來解析包的名字或路徑(如pkg.subpkg)。所以importlib.import_module('..mod', 'pkg.subpkg')將會導入模塊pkg.mod。實際上,該函數是基于importlib.__import__(name, globals=None, locals=None, fromlist=(), level=0)來實現的,區別是,前者返回用戶指定的包或模塊(如pkg.mod),后者返回的是top-level 的包或模塊。為了更深入了解python導入包的過程我們可以結合源碼來分析一下整個過程。
-  
import_module(name, package)根據參數形式確定是相對導入還是絕對導入,并調用importlib包中的_gcd_import(name, package, level)函數
def import_module(name, package=None):"""Import a module.當相對導入的時候,才需要用到package參數,作為導入模式時的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)函數進一步調用importlib中的_find_and_load(name, import_)函數。值得注意的是,gcd,即greatest common denominator,也即最大公約數。為什么起這么個名字呢?因為該函數代表了builtin __import__函數和importlib.import_module函數功能的主要相似之處。
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_)函數實現了搜索和加載的功能,最后返回指定的模塊。在搜索開始前,先查詢模塊緩存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)函數根據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)函數循環調用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)函數加載并執行模塊。
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 # 執行模塊前,應該先將模塊放入模塊緩存中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) # 執行模塊代碼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 
因此,我們可以總結該函數的近似實現如下:
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) # 根據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()
該函數使sys.meta_path中緩存的Finders失效,適用于動態導入程序執行之后才創建/安裝的包的場景。
 -  
importlib.reload(module)
重新導入一個已導入的模塊(module),以上面的test_package為例,并修改test_package/_init_.py 如下:
from .data import *print('____root init____') num = 10進入python console 執行下列代碼:
>>> import test_package ____data dir init____ ____root init____ >>> test_package.num 10此時,不要關閉console,并直接修改源碼,將num=10這一行改為num=11,此時重新導入test_package,我們會發現num的值并不會改變:
>>> import test_package >>> test_package.num 10這種情況下則需要用到reload函數來使源碼的修改得到應用:
>>> 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獲得模塊對象并執行module code, 執行load操作導入模塊。
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])))該函數是python3.4的新增函數,用來替代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創建 ModuleSpec對象
 -  
importlib.util.module_from_spec
通過importlib.util.find_spec()測試一個模塊是否可以被導入,如果可以被導入則使用importlib.util.module_from_spec獲得模塊對象并執行module code, 執行load操作導入模塊。
 -  
importlib.machinery.ModuleSpec(name, loader, *, origin=None, loader_state=None, is_package=None)
ModuleSpec是一個模塊的import-system-related 狀態,對于一個模塊,可以通過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__) 模塊在文件系統上的位置
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 加載期間使用的額外模塊特定數據的容器或則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,如果存在,則返回,進行命名綁定即可;
依次循環查詢sys.meta_path中的importers/finders,看是否能處理要導入的模塊/包(不能處理返回None;能處理則返回一個ModuleSpec對象);
這一步可能會比較復雜,當要導入的包既不是built-in也不是frozen包時,則需要利用PathFinder查找搜索路徑sys.path找到相應的包。由于PathFinder本身沒有實現模塊導入功能,找到包之后需要結合sys.path_importer_cache或sys.path_hooks關聯能導入該包的path entry finder
利用上一步返回的ModuleSpec對象(ModuleSpec包含Loader對象)導入包;
基于此,我們就可以很好利用Finder和Loader的概念來自定義模塊/包的導入行為啦。我們可以利用修改import hooks(sys.meta_path, sys.path_hooks)的方法,改變import默認的加載方式。比如,我們可以實現一個包的導入機制,如果該包不存在則利用pypi安裝再導入(更多自定義導入例子,請參考導入數據文件):
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模块的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: python绘制dotplot
 - 下一篇: python文件路径操作及pathlib