5.4. 加载
当一个模块说明被找到时,导入机制将在加载该模块时使用它(及其所包含的加载器)。 下面是导入的加载部分所发生过程的简要说明:
- module = None
- if spec.loader is not None and hasattr(spec.loader, 'create_module'):
- # It is assumed 'exec_module' will also be defined on the loader.
- module = spec.loader.create_module(spec)
- if module is None:
- module = ModuleType(spec.name)
- # The import-related module attributes get set here:
- _init_module_attrs(spec, module)
- if spec.loader is None:
- # unsupported
- raise ImportError
- if spec.origin is None and spec.submodule_search_locations is not None:
- # namespace package
- sys.modules[spec.name] = module
- elif not hasattr(spec.loader, 'exec_module'):
- module = spec.loader.load_module(spec.name)
- # Set __loader__ and __package__ if missing.
- else:
- sys.modules[spec.name] = module
- try:
- spec.loader.exec_module(module)
- except BaseException:
- try:
- del sys.modules[spec.name]
- except KeyError:
- pass
- raise
- return sys.modules[spec.name]
请注意以下细节:
如果在
sys.modules
中存在指定名称的模块对象,导入操作会已经将其返回。在加载器执行模块代码之前,该模块将存在于
sys.modules
中。 这一点很关键,因为该模块代码可能(直接或间接地)导入其自身;预先将其添加到sys.modules
可防止在最坏情况下的无限递归和最好情况下的多次加载。如果加载失败,则该模块 — 只限加载失败的模块 — 将从
sys.modules
中移除。 任何已存在于sys.modules
缓存的模块,以及任何作为附带影响被成功加载的模块仍会保留在缓存中。 这与重新加载不同,后者会把即使加载失败的模块也保留在sys.modules
中。在模块创建完成但还未执行之前,导入机制会设置导入相关模块属性(在上面的示例伪代码中为 “_init_module_attrs”),详情参见 后续部分。
模块执行是加载的关键时刻,在此期间将填充模块的命名空间。 执行会完全委托给加载器,由加载器决定要填充的内容和方式。
在加载过程中创建并传递给 exec_module() 的模块并不一定就是在导入结束时返回的模块 2。
在 3.4 版更改: 导入系统已经接管了加载器建立样板的责任。 这些在以前是由 importlib.abc.Loader.load_module()
方法来执行的。
5.4.1. 加载器
模块加载器提供关键的加载功能:模块执行。 导入机制调用 importlib.abc.Loader.exec_module()
方法并传入一个参数来执行模块对象。 从 exec_module()
返回的任何值都将被忽略。
加载器必须满足下列要求:
如果模块是一个 Python 模块(而非内置模块或动态加载的扩展),加载器应该在模块的全局命名空间 (
module.dict
) 中执行模块的代码。如果加载器无法执行指定模块,它应该引发
ImportError
,不过在exec_module()
期间引发的任何其他异常也会被传播。
在许多情况下,查找器和加载器可以是同一对象;在此情况下 find_spec()
方法将返回一个规格说明,其中加载器会被设为 self
。
模块加载器可以选择通过实现 create_module()
方法在加载期间创建模块对象。 它接受一个参数,即模块规格说明,并返回新的模块对象供加载期间使用。 create_module()
不需要在模块对象上设置任何属性。 如果模块返回 None
,导入机制将自行创建新模块。
3.4 新版功能: 加载器的 create_module()
方法。
在 3.4 版更改: load_module()
方法被 exec_module()
所替代,导入机制会对加载的所有样板责任作出假定。
为了与现有的加载器兼容,导入机制会使用导入器的 load_module()
方法,如果它存在且导入器也未实现 exec_module()
。 但是,load_module()
现已弃用,加载器应该转而实现 exec_module()
。
除了执行模块之外,load_module()
方法必须实现上文描述的所有样板加载功能。 所有相同的限制仍然适用,并带有一些附加规定:
如果
sys.modules
中存在指定名称的模块对象,加载器必须使用已存在的模块。 (否则importlib.reload()
将无法正确工作。) 如果该名称模块不存在于sys.modules
中,加载器必须创建一个新的模块对象并将其加入sys.modules
。在加载器执行模块代码之前,模块 必须 存在于
sys.modules
之中,以防止无限递归或多次加载。如果加载失败,加载器必须移除任何它已加入到
sys.modules
中的模块,但它必须 仅限 移除加载失败的模块,且所移除的模块应为加载器自身显式加载的。
在 3.5 版更改: 当 exec_module()
已定义但 create_module()
未定义时将引发 DeprecationWarning
。
在 3.6 版更改: 当 exec_module()
已定义但 create_module()
未定义时将引发 ImportError
。
5.4.2. 子模块
当使用任意机制 (例如 importlib
API, import
及 import-from
语句或者内置的 import()
) 加载一个子模块时,父模块的命名空间中会添加一个对子模块对象的绑定。 例如,如果包 spam
有一个子模块 foo
,则在导入 spam.foo
之后,spam
将具有一个 绑定到相应子模块的 foo
属性。 假如现在有如下的目录结构:
- spam/
- __init__.py
- foo.py
- bar.py
并且 spam/init.py
中有如下几行内容:
- from .foo import Foo
- from .bar import Bar
则执行如下代码将在 spam
模块中添加对 foo
和 bar
的名称绑定:
- >>> import spam
- >>> spam.foo
- <module 'spam.foo' from '/tmp/imports/spam/foo.py'>
- >>> spam.bar
- <module 'spam.bar' from '/tmp/imports/spam/bar.py'>
按照通常的 Python 名称绑定规则,这看起来可能会令人惊讶,但它实际上是导入系统的一个基本特性。 保持不变的一点是如果你有 sys.modules['spam']
和 sys.modules['spam.foo']
(例如在上述导入之后就是如此),则后者必须显示为前者的 foo
属性。
5.4.3. 模块规格说明
导入机制在导入期间会使用有关每个模块的多种信息,特别是加载之前。 大多数信息都是所有模块通用的。 模块规格说明的目的是基于每个模块来封装这些导入相关信息。
在导入期间使用规格说明可允许状态在导入系统各组件之间传递,例如在创建模块规格说明的查找器和执行模块的加载器之间。 最重要的一点是,它允许导入机制执行加载的样板操作,在没有模块规格说明的情况下这是加载器的责任。
模块的规格说明会作为模块对象的 spec
属性对外公开。 有关模块规格的详细内容请参阅 ModuleSpec
。
3.4 新版功能.
5.4.4. 导入相关的模块属性
导入机制会在加载期间会根据模块的规格说明填充每个模块对象的这些属性,并在加载器执行模块之前完成。
name
name
属性必须被设为模块的完整限定名称。 此名称被用来在导入系统中唯一地标识模块。loader
属性必须被设为导入系统在加载模块时使用的加载器对象。 这主要是用于内省,但也可用于额外的加载器专用功能,例如获取关联到加载器的数据。- 模块的
package
属性必须设定。 其取值必须为一个字符串,但可以与name
取相同的值。 当模块是包时,其package
值应该设为其name
值。 当模块不是包时,对于最高层级模块package
应该设为空字符串,对于子模块则应该设为其父包名。 更多详情可参阅 PEP 366。
该属性取代 name
被用来为主模块计算显式相对导入,相关定义见 PEP 366。 预期它与 spec.parent
具有相同的值。
在 3.6 版更改: package
预期与 spec.parent
具有相同的值。
spec
spec
属性必须设为在导入模块时要使用的模块规格说明。 对spec
的正确设定将同时作用于 解释器启动期间初始化的模块。 唯一的例外是main
,其中的spec
会 在某些情况下设为 None.
当 package
未定义时, spec.parent
会被用作回退项。
3.4 新版功能.
在 3.6 版更改: 当 package
未定义时,spec.parent
会被用作回退项。
path
- 如果模块为包(不论是正规包还是命名空间包),则必须设置模块对象的
path
属性。 属性值必须为可迭代对象,但如果path
没有进一步的用处则可以为空。 如果path
不为空,则在迭代时它应该产生字符串。 有关path
语义的更多细节将在 下文 中给出。
不是包的模块不应该具有 path
属性。
如果设置了 file
,则也可以再设置 cached
属性,后者取值为编译版本代码(例如字节码文件)所在的路径。 设置此属性不要求文件已存在;该路径可以简单地指向应该存放编译文件的位置 (参见 PEP 3147)。
当未设置 file
时也可以设置 cached
。 但是,那样的场景很不典型。 最终,加载器会使用 file
和/或 cached
。 因此如果一个加载器可以从缓存加载模块但是不能从文件加载,那种非典型场景就是适当的。
5.4.5. module.path
根据定义,如果一个模块具有 path
属性,它就是包。
包的 path
属性会在导入其子包期间被使用。 在导入机制内部,它的功能与 sys.path
基本相同,即在导入期间提供一个模块搜索位置列表。 但是,path
通常会比 sys.path
受到更多限制。
path
必须是由字符串组成的可迭代对象,但它也可以为空。 作用于 sys.path
的规则同样适用于包的 path
,并且 sys.path_hooks
(见下文) 会在遍历包的 path
时被查询。
包的 init.py
文件可以设置或更改包的 path
属性,而且这是在 PEP 420 之前实现命名空间包的典型方式。 随着 PEP 420 的引入,命名空间包不再需要提供仅包含 path
操控代码的 init.py
文件;导入机制会自动为命名空间包正确地设置 path
。
5.4.6. 模块的 repr
默认情况下,全部模块都具有一个可用的 repr,但是你可以依据上述的属性设置,在模块的规格说明中更为显式地控制模块对象的 repr。
如果模块具有 spec (spec
),导入机制将尝试用它来生成一个 repr。 如果生成失败或找不到 spec,导入系统将使用模块中的各种可用信息来制作一个默认 repr。 它将尝试使用 module.name
, module.file
以及 module.loader
作为 repr 的输入,并将任何丢失的信息赋为默认值。
以下是所使用的确切规则:
如果模块具有
spec
属性,其中的规格信息会被用来生成 repr。 被查询的属性有 "name", "loader", "origin" 和 "haslocation" 等等。如果模块具有
file
属性,这会被用作模块 repr 的一部分。如果模块没有
file
但是有loader
且取值不为None
,则加载器的 repr 会被用作模块 repr 的一部分。对于其他情况,仅在 repr 中使用模块的
_name
。
在 3.4 版更改: loader.module_repr()
已弃用,导入机制现在使用模块规格说明来生成模块 repr。
为了向后兼容 Python 3.3,如果加载器定义了 module_repr()
方法,则会在尝试上述两种方式之前先调用该方法来生成模块 repr。 但请注意此方法已弃用。
5.4.7. 已缓存字节码的失效
在 Python 从 .pyc
文件加载已缓存字节码之前,它会检查缓存是否由最新的 .py
源文件所生成。 默认情况下,Python 通过在所写入缓存文件中保存源文件的最新修改时间戳和大小来实现这一点。 在运行时,导入系统会通过比对缓存文件中保存的元数据和源文件的元数据确定该缓存的有效性。
Python 也支持“基于哈希的”缓存文件,即保存源文件内容的哈希值而不是其元数据。 存在两种基于哈希的 .pyc
文件:检查型和非检查型。 对于检查型基于哈希的 .pyc
文件,Python 会通过求哈希源文件并将结果哈希值与缓存文件中的哈希值比对来确定缓存有效性。 如果检查型基于哈希的缓存文件被确定为失效,Python 会重新生成并写入一个新的检查型基于哈希的缓存文件。 对于非检查型 .pyc
文件,只要其存在 Python 就会直接认定缓存文件有效。 确定基于哈希的 .pyc
文件有效性的行为可通过 —check-hash-based-pycs
旗标来重载。
在 3.7 版更改: 增加了基于哈希的 .pyc
文件。在此之前,Python 只支持基于时间戳来确定字节码缓存的有效性。