这篇博客的雏形,严格来讲,在我脑海中浮现已有近一年之久,起源于我之前在写一个python模块并用jupyter notebook测试时发现,当在一个session
中通过import
导入模块,修改模块再次通过import
导入该模块时,模块修改并不会生效。至此,通过一番研究发现,python 导入机制(import machinery)对于我们理解python这门语言,有着至关重要的作用。因此,本篇博客主要探讨 python import machinery原理及其背后的应用。
关键字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
模中块内容,只是将
放在调用者的**命名空间(namespace)**中;而在模块(mode.py
)中定义的对象则保留在模块的命名空间中。因此,从调用者那里,只有通过点表示法 (dot notation) 以
作为前缀,才能访问模块中的对象。当然,我们可以通过from
的方式,将模块中定义的对象导入到当前调用者的命名空间中(以下划线_
开头的对象除外):
>>> 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
,还需我们有一个更深入的理解。
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__)
查看;我们可以分别通过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
,那么解释器首先在该函数的最内部作用域中搜索它;x
不在local作用域中,但出现在另一个函数内部的函数中,则解释器将在enclosing function(请参考博客)的作用域中进行搜索;如果上述四个作用域中都没找到x
,则会报错。举例如下:
a = 1
def func():
a = 3
def inner_func():
a = 5
print(a)
inner_func()
print(a)
if __name__ == '__main__':
func()
print(a)
### output
# 5
# 3
# 1
我们再看一个有趣的例子:
_list: list = [1, 2, 3]
_int: int = 4
def func(seq: list, integer: int):
seq[0] = -1
integer += 1
if __name__ == '__main__':
func(_list, _int)
print(_list, _int)
在该例子中,将全局定义的_list
, _int
传入函数func
, 调用执行之后发现,_lis
改变了而_int
却没有发生改变,这是为什么呢?学过C语言的同学应该马上会想到这会不会是传值和传址的区别。很遗憾,严格来讲都不是。因为在python中一切皆为对象,也就是说一个引用指向一个对象,而不是指向一个特定的内存地址。在python中这和可变对象 mutable和不可变对象 immutable有关:
那么如果我想要改变全局空间中的引用呢?这时则可以通过global
和nonlocal
来"修改"对象的作用域:
a = 3
def func():
global a
a += a
b = 5
def inner_func():
nonlocal b
b += b
inner_func()
print(f'b={b}')
if __name__ == '__main__':
func()
print(f'a={a}')
### output
b=10
a=6
所以,global
声明允许函数在全局作用域中访问和修改对象;nonlocal
声明允许在enclosed function中修改enclosing function中的对象。
模块化编程(modular programming),是强调将计算机程序的功能分离成独立的、可相互改变的“模块”(module)的软件设计技术,它使得每个模块都包含着执行预期功能的一个唯一方面(aspect)所必需的所有东西。python使用 函数(Functions),模块(modules)和包(packages)等概念来实现模块化编程,使得代码更具有可维护性和可重用性。在理解python import machinery之前,读者有必要先了解module和packages两个概念。
我们常见的*.py
文件,以及*.pyc
、*.pyo
、*.pyd
、*.pyw
等单个文件都是module
。 在Python中常有三种不同的方式定义module
:
test.py
;re
(推荐阅读,python re 正则表达式);itertools
(推荐阅读,python函数式编程之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语句实现,比如我们编写一个模块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模块导入时,将依次搜索以下路径:
PYTHONPATH
中所包含的目录列表;这些目录以列表的形式存储在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']
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 "" , 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,即常规包,一般是指每一个文件夹或子文件夹下都包含__init__.py
(模块初始化文件,可以为空)。默认情况下,submodules 和 subpackages不会被导入,而通过__init__.py
可以将任意或全部子模块按照你的想法进行导入。比如在我写的一个示例packagepython_dotplot中,可以通过__init__.py
的__all__
默认暴露且只暴露出部分你想暴露的接口。
Namespace package,即命名空间包,该概念是在python 3.3中新增加的特性,是一种将单个Python包拆分到磁盘上多个目录的机制(当然不局限于磁盘空间)。命名空间包隐式建立,即如果你有一个包含.py文件但没有__init__.py
文件的目录,那么该文件夹则是一个命名空间包。
绕了这么远的路,终于来到今天的正题。通常,要在一个模块中使用另一个模块中的代码我们可以通过import
来实现,但这不是唯一的方法,用户可以使用其他的导入机制,如importlib.import_module()
,绕过__import__()
并使用自己的解决方案实现模块导入。那么,python是通过怎样的机制来实现模块的导入呢?
当导入一个特定模块时,首先搜索模块缓存sys.modules
中是否存在需要导入的模块,如果存在则直接返回;如果为None,则抛出ModuleNotFoundError
;如果不存在,python将依次利用finders和loaders查找并加载该模块。
详细来讲,当要导入一个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必须实现以下两个条件:
module.__dict__
);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的对象
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
模块包含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
的具体实现,用来缓存来自文件系统的结果。
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.txt
contents()
函数返回一个可迭代对象,该可迭代对象包含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: # ContextManager
print(file, type(file))
# Output:
# /path/to/test_package/data0.txt
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 = 0
if 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=3
if character != '.':
break
level += 1
return _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 = None
parent = 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 None
spec = _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_path
if 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.modules
for finder in meta_path:
with _ImportLockContext():
try:
find_spec = finder.find_spec
except AttributeError:
spec = _find_spec_legacy(finder, name, path)
if spec is None:
continue
else:
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 spec
else:
if __spec__ is None:
return spec
else:
return __spec__
else:
return spec
else:
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 = True
try:
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:
pass
raise
module = sys.modules.pop(spec.name)
sys.modules[spec.name] = module
_verbose_message('import {!r} # {!r}', spec.name, spec.loader)
finally:
spec._initializing = False
return module
因此,我们可以总结该函数的近似实现如下:
import importlib.util
import sys
def 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:
pass
path = None
if '.' in absolute_name: # 例如为numpy.random时,先import numpy,再回来import子模块random
parent_name, _, child_name = absolute_name.rpartition('.')
parent_module = import_module(parent_name)
path = parent_module.__spec__.submodule_search_locations
for finder in sys.meta_path:
spec = finder.find_spec(absolute_name, path) # 根据submodule_search_locations搜索子模块
if spec is not None:
break
else:
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 sys
name = 'itertools'
module = None
spec = 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 # optional
print(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.random
In [2]: from numpy import random
In [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 sys
class PipFinder:
@classmethod
def 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 None
return util.find_spec(fullname)
sys.meta_path.append(PipFinder)
这时,导入未安装的模块时,将会自动安装相应模块:
In [1]: import parse
Module 'parse' not installed. Attempting to pip install
Collecting parse
Downloading parse-1.19.0.tar.gz (30 kB)
Building wheels for collected packages: parse
Building wheel for parse (setup.py) ... done
Created wheel for parse: filename=parse-1.19.0-py3-none-any.whl size=24581 sha256=90690a858905aa38e2935201a725276fa85e928cb5f6b16236b6a1494985850e
Stored 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