导入系统
一个 module
内的 Python
代码通过 importing
操作就能够访问另一个模块内的代码。 import
语句是发起调用导入机制的最常用方式,但不是唯一的方式。 importlib.import_module()
以及内置的 __import__()
等函数也可以被用来发起调用导入机制。
import
语句结合了两个操作;它先搜索指定名称的模块,然后将搜索结果绑定到当前作用域中的名称。 import
语句的搜索操作定义为对 __import__()
函数的调用并带有适当的参数。 __import__()
的返回值会被用于执行 import
语句的名称绑定操作。
对 __import__()
的直接调用将仅执行模块搜索以及在找到时的模块创建操作。 不过也可能产生某些副作用,例如导入父包和更新各种缓存 (包括 sys.modules
),只有 import
语句会执行名称绑定操作。
当 import
语句被执行时,标准的内置 __import__()
函数会被调用。 其他发起调用导入系统的机制 (例如 importlib.import_module()
) 可能会选择绕过 __import__()
并使用它们自己的解决方案来实现导入机制。
importlib
importlib
模块提供了一个丰富的 API
用来与导入系统进行交互。 例如 importlib.import_module()
提供了相比内置的 __import__()
更推荐、更简单的 API
用来发起调用导入机制。
包
Python 只有一种模块对象类型,所有模块都属于该类型,无论模块是用 Python、C 还是别的语言实现。 为了帮助组织模块并提供名称层次结构,Python 还引入了 包 的概念。
你可以把包看成是文件系统中的目录,并把模块看成是目录中的文件,但请不要对这个类似做过于字面的理解,因为包和模块不是必须来自于文件系统。 为了方便理解本文档,我们将继续使用这种目录和文件的类比。 与文件系统一样,包通过层次结构进行组织,在包之内除了一般的模块,还可以有子包。
要注意的一个重点概念是所有包都是模块,但并非所有模块都是包。 或者换句话说,包只是一种特殊的模块。 特别地,任何具有 __path__ 属性的模块都会被当作是包。
常规包
常规包是传统的包类型,它们在 Python3.2
及之前就已存在。 常规包通常以一个包含 __init__.py
文件的目录形式实现。 当一个常规包被导入时,这个 __init__.py
文件会隐式地被执行,它所定义的对象会被绑定到该包命名空间中的名称。 __init__.py
文件可以包含与任何其他模块中所包含的 Python
代码相似的代码, Python
将在模块被导入时为其添加额外的属性。
例如,以下文件系统布局定义了一个最高层级的 parent
包和三个子包:
parent/
__init__.py
one/
__init__.py
two/
__init__.py
three/
__init__.py
导入 parent.one
将隐式地执行 parent/__init__.py
和 parent/one/__init__.py
。 后续导入 parent.two
或 parent.three
则将分别执行 parent/two/__init__.py
和 parent/three/__init__.py
导入 parent.one
将隐式地执行 parent/__init__.py
和 parent/one/__init__.py
。 后续导入 parent.two
或 parent.three
则将分别执行 parent/two/__init__.py
和 parent/three/__init__.py
。
命名空间包
没看懂
https://docs.python.org/zh-cn/3/reference/import.html#namespace-packages
搜索
为了开始搜索,Python 需要被导入模块(或者包,对于当前讨论来说两者没有差别)的完整 限定名称。 此名称可以来自 import
语句所带的各种参数,或者来自传给 importlib.import_module()
或 __import__()
函数的形参。
此名称会在导入搜索的各个阶段被使用,它也可以是指向一个子模块的带点号路径,例如 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
将继续搜索该模块。
sys.modules
是可写的。删除键可能不会破坏关联的模块(因为其他模块可能会保留对它的引用),但它会使命名模块的缓存条目无效,导致 Python
在下次导入时重新搜索命名模块。键也可以赋值为 None
,强制下一次导入模块导致 ModuleNotFoundError
。
查找器和加载器
如果指定名称的模块在 sys.modules
找不到,则将发起调用 Python
的导入协议以查找和加载该模块。 此协议由两个概念性模块构成,即 查找器 和 加载器。 查找器的任务是确定是否能使用其所知的策略找到该名称的模块。 同时实现这两种接口的对象称为 导入器 —— 它们在确定能加载所需的模块时会返回其自身。
Python 包含了多个默认查找器和导入器, sys.meta_path
返回一个包含所有查找器的列表,第一个知道如何定位内置模块,第二个知道如何定位冻结模块。 第三个默认查找器会在 import path 中搜索模块。
导入机制是可扩展的,因此可以加入新的查找器以扩展模块搜索的范围和作用域。
查找器并不真正加载模块。 如果它们能找到指定名称的模块,会返回一个 模块规格说明,这是对模块导入相关信息的封装,供后续导入机制用于在加载模块时使用。
导入钩子
导入机制被设计为可扩展;其中的基本机制是 导入钩子。 导入钩子有两种类型: 元钩子 和 导入路径钩子。
元钩子在导入过程开始时被调用,此时任何其他导入过程尚未发生,但 sys.modules
缓存查找除外。 这允许元钩子重载 sys.path
过程、冻结模块甚至内置模块。 元钩子的注册是通过向 sys.meta_path
添加新的查找器对象,具体如下所述。
导入路径钩子是作为 sys.path
(或 package.__path__
) 过程的一部分,在遇到它们所关联的路径项的时候被调用。 导入路径钩子的注册是通过向 sys.path_hooks
添加新的可调用对象,具体如下所述。
元路径
当指定名称的模块在 sys.modules
中找不到时,Python 会接着搜索 sys.meta_path
,其中包含元路径查找器对象列表。 这些查找器按顺序被查询以确定它们是否知道如何处理该名称的模块。 元路径查找器必须实现名为 find_spec()
的方法,该方法接受三个参数:名称、导入路径和目标模块(可选)。 元路径查找器可使用任何策略来确定它是否能处理指定名称的模块。
如果元路径查找器知道如何处理指定名称的模块,它将返回一个说明对象。 如果它不能处理该名称的模块,则会返回 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)。
加载
https://docs.python.org/zh-cn/3/reference/import.html#loading
包的相对导入
妈的,又掉进这个坑了,看了半天还是没搞懂,不过现在的默认导入是绝对导入,所以为什么要折腾自己呢,跳过这一小节不香吗?