import语句的语法格式
- import语句是发起调用importing(导入机制)的常用方式,但并非唯一的方式,
importlib.import_module()
也可以被用来发起调用导入机制. - import的语法范式如下:
imiimport_stmt ::= "import" module ["as" identifier] ("," module ["as" identifier])*
| "from" relative_module "import" identifier ["as" identifier]
("," identifier ["as" identifier])*
| "from" relative_module "import" "(" identifier ["as" identifier]
("," identifier ["as" identifier])* [","] ")"
| "from" module "import" "*"
module ::= (identifier ".")* identifier
relative_module ::= "."* module | "."+
使用改进的 BNF 语法标注描述, 每条规则的开头是一个名称 (即该规则所定义的名称) 加上 ::=。竖线 (|) 被用来分隔可选项;它是此标注中最灵活的操作符。星号 (
*
) 表示前一项的零次或多次重复;类似地,加号 (+) 表示一次或多次重复,而由方括号括起的内容 ([ ]) 表示出现零次或一次 (或者说,这部分内容是可选的)。*
和 + 操作符的绑定是最紧密的;圆括号用于分组。固定字符串包含在引号内。空格的作用仅限于分隔形符。每条规则通常为一行;有许多个可选项的规则可能会以竖线为界分为多行。
- 按照是否带有
from
可以将语句分为基本import语句和from形式两种 - 当使用
importitem.subitem.subsubitem
这样的语法时,除了最后一项之外的每一项都必须是一个包,最后一项可以是模块或包,但不能是前一项中定义的类或函数或变量 - 当使用
frompackageimportitem
时,item可以是包的子模块(或子包),也可以是包中定义的其他名称,如函数,类或变量。 - 每种import形式的语句都可以通过在要导入的
identifier
后加上asnew_identifier
形式的语句给导入的标识符起一个别名 - 通过
,
分割可以在一个import语句中进行多个标识符(可以视为标识符列表)的导入, 使用from形式导入时,标识符列表可以用()
括起来 - 可以通过
frommoduleimport*
调入module
下所有非以下划线(_)开头的名称,但通常不会使用,因为它在解释器中引入了一组未知的名称,而它们很可能会覆盖一些你已经定义过的东西
几种比较特殊的导入方式
- 通配符形式的导入
frommoduleimport*
- 使用
frommoduleimport*
, 在模块中定义的全部公有名称都将被绑定到import 语句所在的局部命名空间。 - 如果一个包的
__init__.py
代码或者在模块中定义了一个名为__all__
的列表,它会被视为在遇到frompackageimport*
时应该导入的模块名列表。 - 如果没有定义
__all__
,fromsound.effectsimport*
语句不会从包sound.effects
中导入所有子模块到当前命名空间;它只确保导入了包sound.effects
,会运行在__init__.py
中的初始化代码,然后导入包中定义的全部名称。 - 虽然通常我们会把import语句写在
.py
文件的头部,但是python并没有做出限制,实际上可以在代码中间进行导入,但是frommoduleimport*
仅在模块层级上被允许, 意思是不能在def
或class
作用域下的import
语句中使用通配符形式的导入,这种做法将引发 SyntaxError。 - 包支持另一个特殊属性
__path__
。它被初始化为一个列表,其中包含在执行该文件中的代码之前保存包的文件__init__.py
的目录的名称 - 相对路径导入
- 当指定要导入哪个模块时,你不必指定模块的绝对名称。当一个模块或包被包含在另一个包之中时,可以在同一个最高层级包中进行相对导入,而不必提及包名称。通过在 from 之后指定的模块或包中使用前缀点号,你可以在不指定确切名称的情况下指明在当前包层级结构中要上溯多少级。一个前缀点号表示是执行导入的模块所在的当前包,两个点号表示上溯一个包层级。三个点号表示上溯两级,依此类推。因此如果你执行 from . import mod 时所处位置为 pkg 包内的一个模块,则最终你将导入 pkg.mod。如果你执行 from ..subpkg2 import mod 时所处位置为 pkg.subpkg1 则你将导入 pkg.subpkg2.mod
- 相对导入是基于当前模块的名称进行导入的。由于主模块的名称总是 "main" ,因此用作Python应用程序主模块的模块必须始终使用绝对导入
- 相对导入使用前缀点号。一个前缀点号表示相对导入从当前包开始。两个或更多前缀点号表示对当前包的上级包的相对导入,第一个点号之后的每个点号代表一级
- 动态导入模块
- importlib.import_module() 被提供用来为动态地确定要导入模块的应用提供支持
模块和包
模块对象
- 模块通常的表现形式是一个包含Python定义和语句的文件。文件名就是模块名后跟文件后缀
.py
。 - 模块对象是Python代码的基本组织单元,在python代码运行时,模块对象由导入系统创建,保存在
sys.modules
(一个字典,将模块名称映射到已加载的模块)当中. - 模块可以包含可执行的语句以及函数定义。这些可执行的语句用于初始化模块。它们仅在模块 第一次 在 import 语句中被导入时才执行。
- 每个模块都有它自己的私有符号表,该表用作模块中定义的所有函数的全局符号表。因此,模块的作者可以在模块内使用全局变量,而不必担心与用户的全局变量发生意外冲突。另一方面,如果你知道自己在做什么,则可以用跟访问模块内的函数的同样标记方法,去访问一个模块的全局变量,modname.itemname。
- 模块可以导入其它模块。习惯上但不要求把所有 import 语句放在模块(或脚本)的开头。被导入的模块名存放在调入模块的全局符号表中。
包的概念
- python引入包的概念,仅仅是为了帮助组织模块并提供名称层次结构,包仅仅是一个概念,而不存在包对象这种东西.所有的包都是模块,包只是一种特殊的模块,可以说,任何具有
__path__
属性的模块都会被当作是包。Python 定义了两种类型的包常规包
和命名空间包
。 - 常规包
- 常规包通常以一个包含
__init__.py
文件的目录形式实现。当一个常规包被导入时,这个__init__.py
文件会隐式地被执行,它所定义的对象会被绑定到该包命名空间中的名称。 - 命名空间包(只需了解)
- 命名空间包是由多个Partion构成的,每个部分为父包增加一个子包。各个部分可能处于文件系统的不同位置。部分也可能处于
zip
文件中、网络上,或者 Python 在导入期间可以搜索的其他地方。命名空间包并不一定会直接对应到文件系统中的对象;它们有可能是无实体表示的虚拟模块。 - 命名空间包的
__path__
属性不使用普通的列表。而是使用定制的可迭代类型,如果其父包的路径 (或者最高层级包的 sys.path) 发生改变,这种对象会在该包内的下一次导入尝试时自动执行新的对包部分的搜索。 - 命名空间包没有
parent/__init__.py
文件。实际上,在导入搜索期间可能找到多个parent
目录,每个都由不同的部分所提供。因此parent/one
的物理位置不一定与parent/two
相邻。在这种情况下Python将为顶级的parent
包创建一个命名空间包,无论是它本身还是它的某个子包被导入。
模块扩展知识
- 标准模块
- Python附带了一个标准模块库,在单独的文档Python库参考(以下称为“库参考”)中进行了描述。一些模块内置于解释器中;它们提供对不属于语言核心但仍然内置的操作的访问,以提高效率或提供对系统调用等操作系统原语的访问。这些模块的集合是一个配置选项,它也取决于底层平台。例如,winreg 模块只在Windows操作系统上提供。一个特别值得注意的模块 sys,它被内嵌到每一个Python解释器中。变量 sys.ps1 和 sys.ps2 定义用作主要和辅助提示的字符串:这两个变量只有在编译器是交互模式下才被定义。
- sys.path 变量是一个字符串列表,用于确定解释器的模块搜索路径。该变量被初始化为从环境变量 PYTHONPATH 获取的默认路径,或者如果 PYTHONPATH 未设置,则从内置默认路径初始化。
dir()
函数- 内置函数
dir()
用于查找模块定义的名称。它返回一个排序过的字符串列表 - 如果没有参数,
dir()
会列出你当前定义的名称: dir()
不会列出内置函数和变量的名称。如果你想要这些,它们的定义是在标准模块builtins
中(dir(builtins))
- 模块对象的属性
- 预定义的 (可写) 属性:
__name__
: 为模块的完整限定名称, 此名称被用来在sys.modules
中唯一地标识模块__doc__
: 为模块的文档字符串,如果没有则为 None;__loader__
: 导入系统在加载模块时使用的加载器对象__package__
: 其取值必须为一个字符串,但可以与__name__
取相同的值。当模块是包时,其__package__
值应该设为其__name__
值。当模块不是包时,对于最高层级模块__package__
应该设为空字符串,对于子模块则应该设为其父包名。__spec__
: 必须设为在导入模块时要使用的模块规格说明,目的是基于每个模块来封装这些导入相关信息。__path__
:- 如果一个模块具有
__path__
属性,它就是包,__path__
包含在执行该文件中的代码之前保存包的文件__init__.py
的目录的名称 - 包的
__path__
属性会在导入其子包期间被使用。在导入机制内部,它的功能与sys.path
基本相同,即在导入期间提供一个模块搜索位置列表 __path__
必须是由字符串组成的可迭代对象,但它也可以为空。作用于sys.path
的规则同样适用于包的__path__
__file__
__cached__
: 可选项。如果设置,此属性的值必须为字符串__file__
是模块对应的被加载文件的路径名,如果它是加载自一个文件的话。某些类型的模块可能没有__file__
属性,例如C
模块是静态链接到解释器内部的; 对于从一个共享库动态加载的扩展模块来说该属性为该共享库文件的路径名。- 如果设置了
__file__
,则也可以再设置__cached__
属性, 取值为编译版本代码(例如字节码文件)所在的路径。设置此属性不要求文件已存在;该路径可以简单地指向应该存放编译文件的位置 __annotations__
(可选) 为一个包含 变量标注 的字典,它是在模块体执行时获取的;
- 特殊的只读属性:
__dict__
为以字典对象表示的模块命名空间。- 模块对象具有由字典对象实现的命名空间(这是被模块中定义的函数的
__globals__
属性引用的字典)。属性引用被转换为该字典中的查找,例如m.x
相当于m.__dict__["x"]
。模块对象不包含用于初始化模块的代码对象(因为初始化完成后不需要它)。 - 属性赋值会更新模块的命名空间字典,例如
m.x=1
等同于m.__dict__["x"]=1
。 - 由于 CPython 清理模块字典的设定,当模块离开作用域时模块字典将会被清理,即使该字典还有活动的引用。想避免此问题,可复制该字典或保持模块状态以直接使用其字典。
- “编译过的”Python文件
- 为了加速模块载入,Python在
__pycache__
目录里缓存了每个模块的编译后版本,名称为 module.version.pyc ,其中名称中的版本字段对编译文件的格式进行编码;它一般使用Python版本号。例如,在CPython版本3.3中,spam.py的编译版本将被缓存为__pycache__/spam.cpython-33.pyc
。此命名约定允许来自不同发行版和不同版本的Python的已编译模块共存。 - Python在两种情况下不会检查缓存。首先,对于从命令行直接载入的模块,它从来都是重新编译并且不存储编译结果;其次,如果没有源模块,它不会检查缓存。为了支持无源文件(仅编译)发行版本, 编译模块必须是在源目录下,并且绝对不能有源模块。
- 已缓存字节码的失效
- 在 Python 从 .pyc 文件加载已缓存字节码之前,它会检查缓存是否由最新的 .py 源文件所生成。默认情况下,Python 通过在所写入缓存文件中保存源文件的最新修改时间戳和大小来实现这一点。在运行时,导入系统会通过比对缓存文件中保存的元数据和源文件的元数据确定该缓存的有效性。
- Python 也支持“基于哈希的”缓存文件,即保存源文件内容的哈希值而不是其元数据。存在两种基于哈希的 .pyc 文件:检查型和非检查型。对于检查型基于哈希的 .pyc 文件,Python 会通过求哈希源文件并将结果哈希值与缓存文件中的哈希值比对来确定缓存有效性。如果检查型基于哈希的缓存文件被确定为失效,Python 会重新生成并写入一个新的检查型基于哈希的缓存文件。对于非检查型 .pyc 文件,只要其存在 Python 就会直接认定缓存文件有效。确定基于哈希的 .pyc 文件有效性的行为可通过 --check-hash-based-pycs 旗标来重载。
当导入模块时,都发生了什么?
importlib
标准库提供了可移植到任何Python解释器的import实现,下面简单介绍一下import语句的执行过程- 基本import语句(不带from)的执行过程:
- 查找一个模块,如果有必要还会加载并初始化模块。
- 包含输入脚本的目录(或者未指定文件时的当前目录)
- PYTHONPATH环境变量(一个包含目录名称的列表,它和shell变量
PATH
有一样的语法) - 取决于安装的默认设置
- 查找模块即执行模块搜索,简单举例来讲,当一个名为
spam
的模块被导入的时候,解释器首先寻找具有该名称的内置模块
。如果没有找到,然后解释器从sys.path
变量给出的目录列表里寻找名为spam
的.py文件
或文件夹。sys.path
初始有这些目录地址: - 在初始化后,Python程序会更改
sys.path
将包含正在运行脚本的文件目录被放在搜索路径的开头处, 在标准库路径之前。这意味着将加载此目录里的脚本可能覆盖标准库的命名空间,导致标准库无法使用. - 这一步如果失败,则可能说明模块无法找到,或者 是在初始化模块,包括执行模块代码期间发生了错误。
在局部命名空间中为 import 语句发生位置所处的作用域定义一个或多个名称。
- 如果模块名称之后带有as(例如
importfoo.bar.bazasfbb
),则跟在as之后的名称将直接绑定到所导入的模块。 - 如果没有指定其他名称,且被导入的模块为最高层级模块(例如
importfoo
),则模块的名称将被绑定到局部命名空间作为对所导入模块的引用。 - 如果被导入的模块不是最高层级模块(例如
importfoo.bar.baz
),则包含该模块的最高层级包的名称(foo
)将被绑定到局部命名空间作为对该最高层级包的引用。所导入的模块必须使用其完整限定名称来访问而不能直接访问。 - 使用逗号分隔的字句将对每个字句分别执行上面的两个过程,与独立使用的import语句是一样的(建议使用独立的语句,代码更清晰)
- 仔细考虑以上过程,我们会发现当通过
importa
导入一个仅包含了空__init__.py
文件的包a
时,是无法导入包a
中的模块a1
的(无法调用a.a1
),只能通过importa.a1
或者fromaimporta1
才能导入a1
,在使用某些扩展库的时候就会遇到无法直接通过包名引用全部内容的情况 - 带from的导入形式的import语句的执行过程:
- 查找 from 子句中指定的模块,如有必要还会加载并初始化模块;
- 查找过程的关键在于模块查找路径列表,与基本import语句是一样的
对于 import 子句中指定的每个标识符:
- 例如
fromfooimportattr
会把attr引入命名空间,但不会引入foo - 这一步相当重要,例如
fromfooimportbar
,会在foo
的__init__.py
所在的目录下再次进行模块搜索,这个目录保存在foo.bar
模块的__path__
属性,而不是sys.path
中 - 例如
fromfooimportattr
属性查找就是hasattr(foo,attr)
- 检查被导入模块是否有该名称的属性
- 如果没有,尝试导入具有该名称的子模块,然后再次检查被导入模块是否有该属性
- 如果未找到该属性,则引发 ImportError。
- 否则的话,将对该值的引用存入局部命名空间,如果有 as 子句则使用其指定的名称,否则使用该属性的名称
- 下面是一段测试代码,可以验证
importa
和fromaimporta1
的区别
# package_test
import a
# 可以正常执行
print("hasattr(a, 'a1')", hasattr(a, 'a1'))
# 可以正常执行
a.a1.func()
try:
assert hasattr(a, 'a2')
except AssertionError:
print("hasattr(a, 'a2')会报错,因为包a(包实际是一种特殊的模块)无法关联到a2,这是由python的导入流程决定的")
from a import a2
# 通过这样的方式直接导入a2模块是可以搜索到a2模块的
# 因为from语句会有一个尝试导入具有该名称的子模块的过程
# 所以通过from语句可以正常导入
# 并且此时a就有a2这个属性了
print("hasattr(a, 'a2')", hasattr(a, 'a2'))
a2.func()
# __init__.py
from . import a1
# a1.py
def func():
print('a1.func')
# a2.py
def func():
print('a2.func')
# 文件夹结构如下
C:.
│ package_test.py
│
└─a
a1.py
a2.py
__init__.py
更详细的搜索过程
- 为了开始搜索,Python需要被导入模块(或者包)的
完整限定名称
。此名称可以来自import语句
所带的各种参数,或者来自传给 importlib.import_module()函数的形参。 - 此名称会在导入搜索的各个阶段被使用,它也可以是指向一个子模块的带点号路径,例如
foo.bar.baz
。在这种情况下,Python会先尝试导入foo
,然后是foo.bar
,最后是foo.bar.baz
。如果这些导入中的任何一个失败,都会引发 ModuleNotFoundError。 - 在导入搜索期间首先会被检查的地方是
sys.modules
。这个字典起到缓存之前导入的所有模块的作用(包括其中间路径)。因此如果之前导入过foo.bar.baz
,则sys.modules
将包含foo
,foo.bar
和foo.bar.baz
条目。每个键的值就是相应的模块对象。
- 在导入期间,会在
sys.modules
查找模块名称,如存在则其关联的值就是需要导入的模块,导入过程完成。然而,如果值为None
,则会引发ModuleNotFoundError
。如果找不到指定模块名称,Python将继续搜索该模块。 importlib.reload()
将重用同一个模块对象
,并简单地通过重新运行模块的代码来重新初始化模块内容。- 当使用任意机制加载一个子模块时,父模块的命名空间中会添加一个对子模块对象的绑定。例如,如果包
spam
有一个子模块foo
,则在导入spam.foo
之后,spam
将具有一个 绑定到相应子模块的foo
属性 - 按照通常的Python名称绑定规则,这看起来可能会令人惊讶,但它实际上是导入系统的一个基本特性。保持不变的一点是如果你有
sys.modules['spam']
和sys.modules['spam.foo']
(例如在上述导入之后就是如此),则后者必须显示为前者的foo
属性
当指定名称的模块在 sys.modules
中找不到时,Python 会接着搜索 sys.meta_path
,其中包含 元路径查找器对象列表
。这些查找器按顺序被查询以确定它们是否知道如何处理该名称的模块。
- 如果
元路径查找器
知道如何处理指定名称的模块,它将返回一个说明对象
。如果它不能处理该名称的模块,则会返回None
。如果sys.meta_path
处理过程到达列表末尾仍未返回说明对象,则将引发ModuleNotFoundError
。引发任何其他异常将直接向上传播,并放弃导入过程。 元路径查找器
的find_spec()
方法调用带有两到三个参数。第一个是被导入模块的完整限定名称
,例如foo.bar.baz
。第二个参数是供模块搜索使用的路径条目。对于最高层级模块, 第二个参数为None
,但对于子模块或子包,第二个参数为父包__path__
属性的值。如果相应的__path__
属性无法访问,将引发ModuleNotFoundError
。(在这里处理了包的子模块的导入)第三个参数是一个将被作为稍后加载目标的现有模块对象,导入系统仅会在重加载期间传入一个目标模块。- 对于单个导入请求可以多次遍历元路径。例如,假设所涉及的模块都尚未被缓存,则导入
foo.bar.baz
将首先执行顶级的导入,在每个元路径查找器mpf
上调用mpf.find_spec("foo",None,None)
。在导入foo
之后,foo.bar
将通过第二次遍历元路径来导入,调用mpf.find_spec("foo.bar",foo.__path__,None)
。一旦foo.bar
完成导入,最后一次遍历将调用mpf.find_spec("foo.bar.baz",foo.bar.__path__,None)
。
Python 的默认 sys.meta_path
具有三种元路径查找器,一种知道如何导入内置模块,一种知道如何导入冻结模块,还有一种知道如何导入来自 importpath
的模块 (即 path based finder
), path based finder
,它会搜索包含一个 路径条目
列表的 importpath
。每个 路径条目
指定一个用于搜索模块的位置。
path based finder
是一种meta path finder
,因此导入机制会通过调用上文描述的基于路径的查找器的find_spec()
方法来启动importpath
搜索。当要向find_spec()
传入path
参数时,它将是一个可遍历的字符串列表,通常为用来在其内部进行导入的包的__path__
属性。如果path
参数为None
,这表示最高层级的导入,将会使用sys.path
path based finder
会负责查找和加载通过path entry
字符串来指定位置的Python模块和包。多数路径条目所指定的是文件系统中的位置,但它们并不必受限于此。它们可以指向URL、数据库查询或可以用字符串指定的任何其他位置。- 有三个变量
sys.path
,sys.path_hooks
和sys.path_importer_cache
由path based finder
所使用。包对象的__path__
属性也会被使用。它们提供了可用于定制导入机制的额外方式。 sys.path
包含一个提供模块和包搜索位置的字符串列表。它初始化自PYTHONPATH
环境变量以及多种其他特定安装和实现的默认设置。sys.path
条目可指定的名称有文件系统中的目录、zip
文件和其他可用于搜索模块的潜在“位置”(参见site
模块),例如URL或数据库查询等。在sys.path
中只能出现字符串和字节串;所有其他数据类型都会被忽略。字节串条目使用的编码由单独的路径条目查找器
来确定。- 基于路径的查找器会迭代搜索路径中的每个条目,并且每次都查找与路径条目对应的
path entry finder
(PathEntryFinder
)。因为这种操作可能很耗费资源(例如搜索会有 stat() 调用的开销),基于路径的查找器会维持一个缓存来将路径条目映射到路径条目查找器。这个缓存放于sys.path_importer_cache
(尽管如此命名,但这个缓存实际存放的是查找器对象而非仅限于importer对象)。通过这种方式,对特定path entry
位置的path entry finder
的高耗费搜索只需进行一次。用户代码可以自由地从sys.path_importer_cache
移除缓存条目,以强制基于路径的查找器再次执行路径条目搜索 - 如果路径条目不存在于缓存中,基于路径的查找器会迭代
sys.path_hooks
中的每个可调用对象。对此列表中的每个路径条目钩子
的调用会带有一个参数,即要搜索的路径条目。每个可调用对象或是返回可处理路径条目的path entry finder
,或是引发ImportError
。基于路径的查找器使用ImportError
来表示钩子无法找到与path entry
相对应的path entry finder
。该异常会被忽略并继续进行importpath
的迭代。 - 如果
sys.path_hooks
迭代结束时没有返回path entry finder
,则基于路径的查找器find_spec()
方法将在sys.path_importer_cache
中存入None
(表示此路径条目没有对应的查找器) 并返回None
,表示此meta path finder
无法找到该模块。 - 如果
sys.path_hooks
中的某个path entry hook
可调用对象的返回值是一个path entry finder
,则以下协议会被用来向查找器请求一个模块的规格说明,并在加载该模块时被使用。 - 当前工作目录 -- 由一个空字符串表示 -- 的处理方式与
sys.path
中的其他条目略有不同。首先,如果发现当前工作目录不存在,则sys.path_importer_cache
中不会存放任何值。其次,每个模块查找会对当前工作目录的值进行全新查找。第三,由sys.path_importer_cache
所使用并由importlib.machinery.PathFinder.find_spec()
所返回的路径将是实际的当前工作目录而非空字符串。 - 作为一种元路径查找器(
meta path finder
),path based finder
实现了上文描述的find_spec()
协议,但是它还对外公开了一些附加钩子,可被用来定制模块如何从importpath
查找和加载。 导入路径钩子
是作为sys.path
(或package.__path__
)过程的一部分,在遇到它们所关联的路径项的时候被调用。导入路径钩子
的注册是通过向sys.path_hooks
添加新的可调用对象- 元钩子在导入过程开始时被调用,此时任何其他导入过程尚未发生,但
sys.modules
缓存查找除外。这允许元钩子重载sys.path
过程、冻结模块甚至内置模块。元钩子的注册是通过向sys.meta_path
(见第4步)添加新的查找器对象
默认的 path entry finder
集合实现了在文件系统中查找模块的所有语义,可处理多种特殊文件类型例如Python 源码 (.py 文件),Python字节码 (.pyc 文件) 以及共享库 (例如 .so 文件)。在标准库中 zipimport
模块的支持下,默认路径条目查找器还能处理所有来自 zip
文件的上述文件类型。
path based finder
自身并不知道如何进行导入。它只是遍历单独的路径条目,将它们各自关联到某个知道如何处理特定类型路径的path entry finder
。path entry finder
模块加载过程
- 当一个
spec
(模块规格)被找到时,导入机制将在加载该模块时使用其所包含的加载器进行加载,加载过程可以由下面的代码简要说明
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”)
_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 中。这一点很关键,因为该模块代码可能(直接或间接地)导入其自身;预先将其添加到 sys.modules 可防止在最坏情况下的无限递归和最好情况下的多次加载。
sys.modules[spec.name] = module
try:
# 模块执行是加载的关键时刻,在此期间将填充模块的命名空间。执行会完全委托给加载器,由加载器决定要填充的内容和方式。
spec.loader.exec_module(module)
except BaseException:
try:
# 如果加载失败,则该模块 -- 只限加载失败的模块 -- 将从 sys.modules 中移除。任何已存在于 sys.modules 缓存的模块,以及任何作为附带影响被成功加载的模块仍会保留在缓存中。这与重新加载不同,后者会把即使加载失败的模块也保留在 sys.modules 中。
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()
的模块并不一定就是在导入结束时返回的模块 - 加载器
loader
- 导入机制调用
importlib.abc.Loader.exec_module()
方法并传入一个参数来执行模块对象。从 exec_module() 返回的任何值都将被忽略。 - 如果模块是一个Python模块(而非内置模块或动态加载的扩展),加载器应该在模块的全局命名空间 (
module.__dict__
) 中执行模块的代码。 - 如果加载器无法执行指定模块,它应该引发
ImportError
,在exec_module()
期间引发的任何其他异常也会被传播。
基本上都参考自python3.9的官方文档,内容相当的枯燥,但是解决了我长期以来的困惑,希望能对你有所帮助吧