Python 导入系统的工作原理

2021-07-24 23:34:01

如果你让我说出 Python 最容易被误解的方面,我会毫不犹豫地回答:Python 导入系统。只要记住你使用了多少次相对导入并得到类似 ImportError 的东西:尝试相对导入而没有已知的父包;或者试图弄清楚如何构建一个项目,以便所有的导入都能正常工作;或者在找不到更好的解决方案时入侵 sys.path。每个 Python 程序员都经历过这样的事情,流行的 StackOverflow 问题,例如我们从不同文件夹导入文件(1822 票)、Python 3 中的相对导入(1064 票)和第 10 亿次相对导入(993 票),都是一个很好的指标其中。 Python 导入系统不仅看起来很复杂——它很复杂。因此,即使文档非常好,它也不能让您全面了解正在发生的事情。得到这样一张图的唯一方法是研究 Python 执行 import 语句时幕后发生的事情。这就是我们今天要做的。注意:在这篇文章中,我指的是 CPython 3.9。随着 CPython 的发展,一些实现细节肯定会发生变化。我将尝试跟踪重要更改并添加更新说明。在我们开始之前,让我向您展示我们计划的更详细版本。首先,我们将讨论导入系统的核心概念:模块、子模块、包、from <> import <> 语句、相对导入等。然后我们将对不同的导入语句进行脱糖,并看到它们最终都调用了内置的 __import__() 函数。最后,我们将研究 __import__() 的默认实现是如何工作的。我们走吧!你认为它有什么作用?您可能会说它导入了一个名为 m 的模块并将该模块分配给变量 m。你会是对的。但究竟什么是模块?什么被分配给变量?为了回答这些问题,我们需要给出更精确的解释:语句 import m 搜索名为 m 的模块,为该模块创建一个模块对象,并将模块对象分配给变量。看看我们如何区分模块和模块对象。我们现在可以定义这些术语。模块是 Python 认为是模块并知道如何为其创建模块对象的任何东西。这包括 Python 文件、目录和用 C 编写的内置模块等内容。我们将在下一节中查看完整列表。我们导入任何模块的原因是因为我们想要访问模块定义的函数、类、常量和其他名称。这些名称必须存储在某处,这就是模块对象的用途。模块对象是一个 Python 对象,它充当模块名称的命名空间。名称存储在模块对象的字典中(可用作 m.__dict__),因此我们可以将它们作为属性访问。

typedef struct { PyObject ob_base ; PyObject * md_dict ; struct PyModuleDef * md_def ;无效 * md_state ; PyObject * md_weaklist ; PyObject * md_name ; } PyModuleObject ; md_dict 字段存储模块的字典。其他领域对我们的讨论并不重要。 Python 为我们隐式地创建了模块对象。为了看看这个过程没有什么神奇之处,让我们自己创建一个模块对象。我们通常通过调用它们的类型来创建 Python 对象,比如 MyClass() 或 set()。模块对象的类型在 C 代码中是 PyModule_Type,但它在 Python 中不能作为内置对象使用。幸运的是,这种“不可用”的类型可以在 types 标准模块中找到: types 模块如何定义 ModuleType?它只是导入 sys 模块(任何模块都可以),然后在返回的模块对象上调用 type() 。我们也可以这样做: 无论我们如何获取 ModuleType,一旦获取,我们就可以轻松创建模块对象: 新创建的模块对象不是很有趣,但预初始化了一些特殊属性: 这些特殊属性主要是由导入系统本身使用,但有些也在应用程序代码中使用。例如,__name__ 属性通常用于获取当前模块的名称:

请注意 __name__ 可用作全局变量。这种观察似乎很明显,但它至关重要。它来自于全局变量字典设置为当前模块的字典的事实:当前模块充当 Python 代码执行的命名空间。当 Python 导入一个 Python 文件时,它会创建一个新的模块对象,然后使用模块对象的字典作为全局变量的字典来执行文件的内容。类似地,Python 在直接执行 Python 文件时,首先会创建一个名为 __main__ 的特殊模块,然后将其字典用作全局变量的字典。因此,全局变量始终是某个模块的属性,从执行代码的角度来看,该模块被认为是当前模块。内置模块是编译成 python 可执行文件的 C 模块。由于它们是可执行文件的一部分,因此它们始终可用。这是他们的主要特点。 sys.builtin_module_names 元组存储它们的名称: $ python -q >>> import sys >>> sys 。内置模块名称('_abc'、'_ast'、'_codecs'、'_collections'、'_functools'、'_imp'、'_io'、'_locale'、'_operator'、'_peg_parser'、'_signal'、'_sre'、 '_stat', '_string', '_symtable', '_thread', '_tracemalloc', '_warnings', '_weakref', 'atexit', 'builtins', 'errno', 'faulthandler', 'gc', 'itertools ', 'marshal', 'posix', 'pwd', 'sys', 'time', 'xxsubtype') 冻结模块也是 python 可执行文件的一部分,但它们是用 Python 编写的。 Python 代码被编译为代码对象,然后将编组后的代码对象合并到可执行文件中。冻结模块的示例是 _frozen_importlib 和 _frozen_importlib_external 。 Python 冻结它们是因为它们实现了导入系统的核心,因此不能像其他 Python 文件一样导入。 C 扩展有点像内置模块,也有点像 Python 文件。一方面,它们是用 C 或 C++ 编写的,并通过 Python/C API 与 Python 交互。另一方面,它们不是可执行文件的一部分,而是在导入期间动态加载。包括数组、数学和选择在内的一些标准模块是 C 扩展。许多其他的,包括 asyncio、heapq 和 json 都是用 Python 编写的,但在幕后调用了 C 扩展。从技术上讲,C 扩展是公开所谓的初始化函数的共享库。它们通常命名为 modname.so,但文件扩展名可能因平台而异。例如,在我的 macOS 上,这些扩展中的任何一个都可以使用:.cpython-39-darwin.so、.abi3.so、.so。在 Windows 上,您会看到 .dll 及其变体。 Python 字节码文件通常与常规 Python 文件一起位于 __pycache__ 目录中。它们是将 Python 代码编译为字节码的结果。更具体地说,.pyc 文件包含一些元数据,后跟模块的编组代码对象。它的目的是通过跳过编译阶段来减少模块的加载时间。 Python 导入 .py 文件时,首先会在 __pycache__ 目录中搜索对应的 .pyc 文件并执行。如果 .pyc 文件不存在,Python 会编译代码并创建文件。

但是,如果我们不能直接执行和导入它们,我们就不会调用 .pyc 文件模块。令人惊讶的是,我们可以: $ ls module.pyc $ python module.pyc 我是 .pyc 文件 $ python -c "import module" 我是 .pyc 文件要了解有关 .pyc 文件的更多信息,请查看 PEP 3147 - - PYC 存储库目录和 PEP 552 - 确定性 pycs。正如我们稍后将看到的,我们可以自定义导入系统以支持更多类型的模块。所以任何东西都可以称为模块,只要 Python 可以为它创建一个模块对象给定模块名称。如果模块名称仅限于像 mymodule 或 utils 这样的简单标识符,那么它们都必须是唯一的,每次给新文件命名时,我们都必须非常认真地思考。出于这个原因,Python 允许模块有子模块和模块名称包含点。它首先导入模块 a 然后导入子模块 ab 它将子模块添加到模块的字典中并将模块分配给变量 a,因此我们可以将子模块作为模块的属性访问。可以有子模块的模块称为包。从技术上讲,包是具有 __path__ 属性的模块。这个属性告诉 Python 在哪里寻找子模块。当 Python 导入顶级模块时,它会在 sys.path 中列出的目录和 ZIP 存档中搜索该模块。但是当它导入一个子模块时,它使用父模块的 __path__ 属性而不是 sys.path。

目录是将模块组织成包的最常见方式。如果一个目录包含一个 __init__.py 文件,它被认为是一个普通的包。当 Python 导入这样一个目录时,它会执行 __init__.py 文件,因此在那里定义的名称成为模块的属性。 __init__.py 文件通常为空或包含与包相关的属性,例如 __doc__ 和 __version__。它还可以用于将包的公共 API 与其内部实现分离。假设您开发了一个具有以下结构的库: 并且您想为库的用户提供两个函数:在 module1.py 中定义的 func1() 和在 module2.py 中定义的 func2()。如果您将 __init__.py 留空,那么用户必须指定子模块来导入函数: 这可能是您想要的,但您可能还希望允许用户导入这样的函数: 一个名为 C 扩展名的目录__init__.so 或带有名为 __init__.pyc 的 .pyc 文件也是一个常规包。 Python 可以完美地导入此类包:在 3.3 版本之前,Python 只有常规包。没有 __init__.py 的目录根本不被视为包。这是一个问题,因为人们不喜欢创建空的 __init__.py 文件。 PEP 420 通过在 Python 3.3 中引入命名空间包使这些文件变得不必要。命名空间包也解决了另一个问题。它们允许开发人员将包的内容放置在多个位置。例如,如果您有以下目录结构:

而且mylibs和morelibs都在sys.path中,那么你可以像这样导入package1和package2:它是如何工作的?当 Python 在模块搜索期间遍历路径中的路径条目( sys.path 或 parent 的 __path__ )时,它会记住与模块名称匹配的没有 __init__.py 的目录。如果遍历所有条目后,找不到常规包、Python 文件或 C 扩展名,它会创建一个模块对象,其 __path__ 包含存储的目录。要求 __init__.py 的最初想法是防止名为 string 或 site 之类的目录遮蔽标准模块。命名空间包不会隐藏其他模块,因为它们在模块搜索期间的优先级较低。除了导入模块之外,我们还可以使用 from <> import <> 语句导入模块属性,如下所示: 该语句导入一个名为 module 的模块,并将指定的属性分配给相应的变量: 注意 module 变量在像被删除一样导入:当 Python 看到一个模块没有指定的属性时,它认为该属性是一个子模块并尝试导入它。因此,如果模块定义了 func 和 Class 但没有定义子模块,Python 将尝试导入 module.submodule。

如果我们不想明确指定要从模块导入的名称,我们可以使用导入的通配符形式:此语句的工作方式就像将“*”替换为模块的所有公共名称一样。这些是模块字典中不以下划线“_”开头的名称或 __all__ 属性中列出的名称(如果已定义)。到目前为止,我们一直通过指定绝对模块名称来告诉 Python 要导入哪些模块。 from <> import <> 语句也允许我们指定相关的模块名称。下面是一些例子: .. 和 ..ab 之类的结构是相对的模块名称,但它们相对于什么?正如我们所说,Python 文件在当前模块的上下文中执行,该模块的字典充当全局变量的字典。当前模块与任何其他模块一样,可以属于一个包。这个包被称为当前包,这就是相对模块名称的相对关系。模块的 __package__ 属性存储模块所属的包的名称。如果模块是一个包,那么该模块属于它自己,而 __package__ 只是模块的名称( __name__ )。如果模块是子模块,则它属于父模块,并且 __package__ 设置为父模块的名称。最后,如果模块既不是包也不是子模块,那么它的包是未定义的。在这种情况下, __package__ 可以设置为空字符串(例如模块是顶级模块)或无(例如模块作为脚本运行)。相对模块名称是前面有一些点的模块名称。一个前导点代表当前包。所以,当 __package__ 被定义时,下面的语句:每个额外的点告诉 Python 从 __package__ 上移一级。如果 __package__ 设置为“ab”,则此语句:

这是因为 Python 不会通过文件系统来解析相对导入。它只需要 __package__ 的值,去掉一些后缀并附加一个新后缀以获得绝对模块名称。显然,当 __package__ 根本没有定义时,相对导入就会中断。在这种情况下,您会收到以下错误:当您将相对导入作为脚本运行程序时,您最常看到它。由于您使用文件系统路径而不是模块名称指定要运行的程序,并且由于 Python 需要模块名称来计算 __package__,因此代码在 __main__ 模块中执行,其 __package__ 属性设置为 None。在运行具有相对导入的程序时避免导入错误的标准方法是使用 -m 开关将其作为模块运行: -m 开关告诉 Python 使用与导入期间相同的机制来查找模块。 Python 获取模块名称并能够计算当前包。例如,如果我们运行一个名为 package.module 的模块,其中 module 指的是一个常规的 .py 文件,那么代码将在 __main__ 模块中执行,其 __package__ 属性设置为“package”。您可以在文档和 PEP 338 中阅读有关 -m 开关的更多信息。好吧。这是一个热身。现在我们将看看当我们导入一个模块时到底发生了什么。如果我们对任何导入语句进行脱糖,我们将看到它最终调用了内置的 __import__() 函数。该函数接受一个模块名称和一堆其他参数,找到该模块并为其返回一个模块对象。至少,这是它应该做的。

Python 允许我们将 __import__() 设置为自定义函数,因此我们可以完全改变导入过程。例如,这是一个破坏一切的更改: >>> import builtins >>>builtins 。 __import__ = None >>> import math Traceback (最近一次调用最后一次): File "<stdin>", line 1, in <module> TypeError: 'NoneType' object is not callable 你很少看到人们因为其他原因覆盖 __import__()而不是记录或调试。默认实现已经提供了强大的自定义机制,我们将只关注它。 __import__() 的默认实现是 importlib.__import__()。嗯,这几乎是真的。 importlib 模块是一个标准模块,它实现了导入系统的核心。它是用 Python 编写的,因为导入过程涉及路径处理和其他你更喜欢用 Python 而不是 C 来做的事情。但出于性能原因,importlib 的一些函数被移植到 C 中。默认 __import__() 实际上调用了 importlib.__import__() 的 C 端口。出于我们的目的,我们可以放心地忽略差异并只研究 Python 版本。在我们这样做之前,让我们看看不同的导入语句如何调用 __import__()。要了解 import 语句的作用,我们可以查看为它生成的字节码,然后通过查看 Python/ceval.c 中的评估循环找出每个字节码指令的作用。 $ echo "import m" | python -m dis 1 0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (None) 4 IMPORT_NAME 0 (m) 6 STORE_NAME 0 (m)... 第一个 LOAD_CONST 指令将 0 压入值堆栈。第二个 LOAD_CONST 推送 None。然后 IMPORT_NAME 指令做了一些我们稍后会研究的事情。最后,STORE_NAME 将值堆栈顶部的值分配给变量 m。

case TARGET (IMPORT_NAME): { PyObject * name = GETITEM ( names , oparg ); PyObject * fromlist = POP(); PyObject * level = TOP(); PyObject * res ; res = import_name ( tstate , f , name , fromlist , level ); Py_DECREF(级别); Py_DECREF ( fromlist ); SET_TOP ( res );如果(res == NULL)转到错误;派遣 ();所有操作都发生在 import_name() 函数中。它调用 __import__() 来完成这项工作,但如果 __import__() 没有被覆盖,它会使用一个快捷方式并调用 importlib.__import__() 的 C 端口,称为 PyImport_ImportModuleLevelObject()。以下是代码中实现此逻辑的方式: static PyObject * import_name ( PyThreadState * tstate , PyFrameObject * f , PyObject * name , PyObject * fromlist , PyObject * level ) { _Py_IDENTIFIER ( __import__ ); PyObject * import_func , * res ; PyObject * 堆栈 [5]; import_func = _PyDict_GetItemIdWithError ( f -> f_builtins , & PyId___import__ ); if ( import_func == NULL ) { if ( !_PyErr_Occurred ( tstate )) { _PyErr_SetString ( tstate , PyExc_ImportError , "__import__ not found");返回 NULL ; } /* 未重载 __import__ 的快速路径。 */ if ( import_func == tstate -> interp -> import_func ) { int ilevel = _PyLong_AsInt ( level ); if ( ilevel == - 1 && _PyErr_Occurred ( tstate )) { return NULL ; res = PyImport_ImportModuleLevelObject ( name , f -> f_globals , f -> f_locals == NULL ? Py_None : f -> f_locals , fromlist , ilevel );返回资源; } Py_INCREF ( import_func );堆栈 [0] = 名称;堆栈 [1] = f -> f_globals ;堆栈 [2] = f -> f_locals == NULL ? Py_None : f -> f_locals ;堆栈 [3] = 来自列表;堆栈 [4] = 级别; res = _PyObject_FastCall ( import_func , stack , 5 ); Py_DECREF ( import_func );返回资源;如果您仔细检查以上所有内容,您将能够得出以下结论: def __import__ ( name , globals = None , locals = None , fromlist = (), level = 0 ): """Import a module . 'globals' 参数用于推断从哪里发生导入以处理相对导入。'locals' 参数被忽略。'fromlist' 参数指定什么 sho ......