Python导入模块加载

2023-11-20 56

当一个模块规范(module specification)被找到后,导入机制将会使用它来加载该模块。模块规范是在模块被找到后生成的对象,它包含了关于如何加载这个模块的所有必要信息,包括模块的名称、所在位置、加载器等。

一、导入的加载部分

当一个模块说明被找到时,导入机制将在加载该模块时使用它(及其所包含的加载器)。 下面是导入的加载部分所发生过程的简要说明:

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)
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]

请注意以下细节:

1、如果在 sys.modules 中存在指定名称的模块对象,导入操作会已经将其返回。

2、在加载器执行模块代码之前,该模块将存在于 sys.modules 中。 这一点很关键,因为该模块代码可能(直接或间接地)导入其自身;预先将其添加到 sys.modules 可防止在最坏情况下的无限递归和最好情况下的多次加载。

3、如果加载失败,则该模块 — 只限加载失败的模块 — 将从 sys.modules 中移除。 任何已存在于 sys.modules 缓存的模块,以及任何作为附带影响被成功加载的模块仍会保留在缓存中。 这与重新加载不同,后者会把即使加载失败的模块也保留在 sys.modules 中。

4、在模块创建完成但还未执行之前,导入机制会设置导入相关模块属性(在上面的示例伪代码中为 “_init_module_attrs”),详情参见 后续部分。

5、模块执行是加载的关键时刻,在此期间将填充模块的命名空间。 执行会完全委托给加载器,由加载器决定要填充的内容和方式。

6、在加载过程中创建并传递给 exec_module() 的模块并不一定就是在导入结束时返回的模块 。

二、加载器

模块加载器在导入机制中提供了关键的加载功能,其中一个重要的功能是执行模块。在导入过程中,当模块加载器被调用时,导入机制会使用importlib.abc.Loader.exec_module()方法来执行模块对象。这个方法接收一个参数,即待执行的模块对象。

在执行模块时,exec_module()方法会执行模块对象所包含的代码,并将其作为一个独立的命名空间中的一部分进行解释和执行。执行的结果,也就是模块中定义的各种变量、函数、类等,将被存储在该命名空间中供其他代码引用。

加载器必须满足下列要求:

  • 如果模块是一个 Python 模块(而非内置模块或动态加载的扩展),加载器应该在模块的全局命名空间 (module.__dict__) 中执行模块的代码;
  • 如果加载器无法执行指定模块,它应该引发 ImportError,不过在 exec_module() 期间引发的任何其他异常也会被传播。

在许多情况下,查找器和加载器可以是同一对象;在此情况下 find_spec() 方法将返回一个规格说明,其中加载器会被设为 self。

模块加载器可以选择通过实现 create_module() 方法在加载期间创建模块对象。 它接受一个参数,即模块规格说明,并返回新的模块对象供加载期间使用。 create_module() 不需要在模块对象上设置任何属性。 如果模块返回 None,导入机制将自行创建新模块。

为了与现有的加载器兼容,导入机制会使用导入器的 load_module() 方法,如果它存在且导入器也未实现 exec_module()。 但是,load_module() 现已弃用,加载器应该转而实现 exec_module()。

除了执行模块之外,load_module() 方法必须实现上文描述的所有样板加载功能。 所有相同的限制仍然适用,并带有一些附加规定:

1、如果 sys.modules 中存在指定名称的模块对象,加载器必须使用已存在的模块。 (否则 importlib.reload() 将无法正确工作。) 如果该名称模块不存在于 sys.modules 中,加载器必须创建一个新的模块对象并将其加入 sys.modules。

2、在加载器执行模块代码之前,模块 必须 存在于 sys.modules 之中,以防止无限递归或多次加载。

3、如果加载失败,加载器必须移除任何它已加入到 sys.modules 中的模块,但它必须 仅限 移除加载失败的模块,且所移除的模块应为加载器自身显式加载的。

三、子模块

当使用任意机制 (例如 importlib API, import 及 import-from 语句或者内置的 __import__()) 加载一个子模块时,父模块的命名空间中会添加一个对子模块对象的绑定。 例如,如果包 spam 有一个子模块 foo,则在导入 spam.foo 之后,spam 将具有一个 绑定到相应子模块的 foo 属性。 假如现在有如下的目录结构:

spam/
__init__.py
foo.py

并且 spam/__init__.py 中有如下几行内容:

from .foo import Foo

那么执行如下代码将把 foo 和 Foo 的名称绑定添加到 spam 模块中:

>>>import spam
>>>spam.foo
<module 'spam.foo' from '/tmp/imports/spam/foo.py'>
>>>spam.Foo
<class 'spam.foo.Foo'>

按照通常的 Python 名称绑定规则,它实际上是导入系统的一个基本特性。 保持不变的一点是如果你有 sys.modules[‘spam’] 和 sys.modules[‘spam.foo’] (例如在上述导入之后就是如此),则后者必须显示为前者的 foo 属性。

四、模块规格说明

导入机制在导入期间会使用有关每个模块的多种信息,特别是加载之前, 大多数信息都是所有模块通用的, 模块规格说明的目的是基于每个模块来封装这些导入相关信息。

在导入期间使用规格说明可允许状态在导入系统各组件之间传递,例如在创建模块规格说明的查找器和执行模块的加载器之间。 最重要的一点是,它允许导入机制执行加载的样板操作,在没有模块规格说明的情况下这是加载器的责任。

五、导入相关的模块属性

导入机制会在加载期间会根据模块的规格说明填充每个模块对象的这些属性,并在加载器执行模块之前完成。建议使用 __spec__ 及其属性来代替下面列出的任何其他单独属性。

  • __name__:__name__ 属性必须被设为模块的完整限定名称。 此名称被用来在导入系统中唯一地标识模块。
  • __loader__:__loader__ 属性必须被设为导入系统在加载模块时使用的加载器对象。 这主要是用于内省,但也可用于额外的加载器专用功能,例如获取关联到加载器的数据。建议使用 __spec__ 来代替这个属性。
  • __package__:模块的 __package__ 属性可以被设置, 其值必须为一个字符串,但可以与 __name__ 取相同的值。 当模块是一个包时,其 __package__ 值应当设为其 __name__ 值。 当模块不是包时,对于最高层级模块 __package__ 应当设为空字符串,对于子模块则应当设为其父包名。
  • __spec__:__spec__ 属性必须设为在导入模块时要使用的模块规格说明。 对 __spec__ 的正确设定将同时作用于 解释器启动期间初始化的模块。 唯一的例外是 __main__,其中的 __spec__ 会 在某些情况下设为 None。当 __spec__.parent 未设置时,将使用 __package__ 作用回退项。
  • __path__:如果模块为包(不论是正规包还是命名空间包),则必须设置模块对象的 __path__ 属性。 属性值必须为可迭代对象,但如果 __path__ 没有进一步的用处则可以为空。 如果 __path__ 不为空,则在迭代时它应该产生字符串。

不是包的模块不应该具有 __path__ 属性。

  • __file__
  • __cached__:__file__ 是可选项(如果设置,其值必须为字符串)。 它指明要载入的模块所在文件的路径(如果是从文件载入),或者对于从共享库动态载入的扩展模块来说则是共享库文件的路径。 它对特定类型的模块来说可能是缺失的,例如静态链接到解释器中的 C 模块,并且导入系统也可能会在它没有语法意义时选择不设置它(例如是从数据库导入的模块)。

如果设置了 __file__ 则 __cached__ 属性也可能会被设置,它是指向任何代码的已编译版本的路径(即字节码文件)。 设置此属性并不需要存在相应的文件;该路径可以简单地指向已编译文件将要存在的位置 。

注意: __cached__ 即使在未设置 __file__ 时也可能会被设置。 但是,那样的场景是非典型的。 最终,加载器会是查找器 (__file__ 和 __cached__ 也是自它派生的) 所提供的模块规格说明的使用方。 因此如果一个加载器可以从缓存加载模块但是不能从文件加载,那样的非典型场景就是适当的。建议使用 __spec__ 来代替 __cached__。

六、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__。

七、模块的repr

默认情况下,全部模块都具有一个可用的 repr,但是可以依据上述的属性设置,在模块的规格说明中更为显式地控制模块对象的 repr。

如果模块具有 spec (__spec__),导入机制将尝试用它来生成一个 repr。 如果生成失败或找不到 spec,导入系统将使用模块中的各种可用信息来制作一个默认 repr。 它将尝试使用 module.__name__, module.__file__ 以及 module.__loader__ 作为 repr 的输入,并将任何丢失的信息赋为默认值。

以下是所使用的确切规则:

  • 如果模块具有 __spec__ 属性,其中的规格信息会被用来生成 repr。 被查询的属性有 “name”, “loader”, “origin” 和 “has_location” 等等;
  • 如果模块具有 __file__ 属性,这会被用作模块 repr 的一部分;
  • 如果模块没有 __file__ 但是有 __loader__ 且取值不为 None,则加载器的 repr 会被用作模块 repr 的一部分;
  • 对于其他情况,仅在 repr 中使用模块的 __name__。

八、已缓存字节码的失效

在 Python 从 .pyc 文件加载已缓存字节码之前,它会检查缓存是否由最新的 .py 源文件所生成。 默认情况下,Python 通过在所写入缓存文件中保存源文件的最新修改时间戳和大小来实现这一点。 在运行时,导入系统会通过比对缓存文件中保存的元数据和源文件的元数据确定该缓存的有效性。

Python 也支持“基于哈希的”缓存文件,即保存源文件内容的哈希值而不是其元数据。 存在两种基于哈希的 .pyc 文件:检查型和非检查型。 对于检查型基于哈希的 .pyc 文件,Python 会通过求哈希源文件并将结果哈希值与缓存文件中的哈希值比对来确定缓存有效性。 如果检查型基于哈希的缓存文件被确定为失效,Python 会重新生成并写入一个新的检查型基于哈希的缓存文件。 对于非检查型 .pyc 文件,只要其存在 Python 就会直接认定缓存文件有效。 确定基于哈希的 .pyc 文件有效性的行为可通过 –check-hash-based-pycs 旗标来重载。

  • 广告合作

  • QQ群号:707632017

温馨提示:
1、本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com(#改为@)。 2、本站原创内容未经允许不得转裁,转载请注明出处“站长百科”和原文地址。