快捷搜索:  汽车  科技

pythonimport执行流程(Python幕后Python导入import的工作原理)

pythonimport执行流程(Python幕后Python导入import的工作原理)导入任何模块的原因是因为想要访问模块定义的函数、类、常量和其他名称。这些名称必须存储在某处,这就是模块对象的用途。一个模块对象是Python对象充当模块的名称命名空间。名称存储在模块对象的字典中(m.__dict__),因此可以将它们作为属性访问。python认定一个模块,并知道如何创建一个模块对象。模块包括 Python 文件、目录和用 C 编写的内置模块等。在开始之前,首先,将讨论import系统的核心概念:modules submodules packages from <> import <> 语句 relative imports等。然后将对不同的import语句进行解剖,并看到它们最终都调用了内置__import__()函数。最后,将研究默认实现的__import__()工作原理。考虑一个简单的导入语句:import m它有什么作用?你可能会说它

更多互联网精彩资讯、工作效率提升关注【飞鱼在浪屿】(日更新)

pythonimport执行流程(Python幕后Python导入import的工作原理)(1)

Python 最容易被误解的方面其中之一是import。

Python 导入系统不仅看起来很复杂。因此,即使文档非常好,它也不能让您全面了解正在发生的事情。唯一方法是研究 Python 执行 IMPORT 语句时幕后发生的事情。

注意:在这篇文章中,指的是 CPython 3.9。随着 CPython 的发展,一些实现细节肯定会发生变化。


开始之前

在开始之前,首先,将讨论import系统的核心概念:modules submodules packages from <> import <> 语句 relative imports等。然后将对不同的import语句进行解剖,并看到它们最终都调用了内置__import__()函数。最后,将研究默认实现的__import__()工作原理。


模块和模块对象

考虑一个简单的导入语句:

import m

它有什么作用?你可能会说它导入一个名为m的模块并将该模块分配给变量m。你是对的,但究竟什么是模块?什么被分配给变量?为了回答这些问题,我们需要给出更精确的解释:该语句import m搜索名为m的模块,为该模块创建一个模块对象,并将模块对象分配给变量。看看如何区分模块和模块对象。我们现在可以定义这些术语。

python认定一个模块,并知道如何创建一个模块对象。模块包括 Python 文件、目录和用 C 编写的内置模块等。

导入任何模块的原因是因为想要访问模块定义的函数、类、常量和其他名称。这些名称必须存储在某处,这就是模块对象的用途。一个模块对象是Python对象充当模块的名称命名空间。名称存储在模块对象的字典中(m.__dict__),因此可以将它们作为属性访问。

$ python -q >>> from types import ModuleType >>> ModuleType <class 'module'>

>>>import sys >>> ModuleType = type(sys) >>> ModuleType <class 'module'>

一旦获取ModuleType,我们就可以轻松创建一个模块对象:

>>> m = ModuleType ( 'm' ) >>> m <module 'm'>

一个新创建的模块对象需要一些预先初始化的特殊属性:

>>>m.__dict__ {'__name__':'m','__doc__':None,'__package__':None,'__loader__':None,'__spec__':None}

大多数这些特殊属性主要由import系统本身使用,但也有一些在应用程序代码中使用。__name__例如,该属性通常用于获取当前模块的名称:

>>> __name__ '__main__'

请注意,__name__可用作全局变量。它来自于全局变量的字典.

>>>import sys >>> current_module = sys.modules [ __name__ ] # sys.modules 存储导入的模块 >>> current_module.__dict__ is globals() True

当前模块充当 Python 代码执行的命名空间。当 Python 导入一个 Python 文件时,它会创建一个新的模块对象,然后使用模块对象的字典作为全局变量的字典来执行文件的内容。类似地,Python 在直接执行 Python 文件时,首先会创建一个特殊的模块调用__main__,然后将其字典用作全局变量的字典。因此,全局变量始终是某个模块的属性,从执行代码的角度来看,该模块被认为是current module当前模块


不同类型的模块

默认情况下,Python 将以下内容识别为模块:

  1. 内置modules。
  2. 冻结modules。
  3. C 扩展。
  4. Python 源代码文件(.py文件)。
  5. Python 字节码文件(.pyc文件)。
  6. 目录。

内置模块是编译成python可执行文件的C 模块。由于它们是可执行文件的一部分,因此它们始终可用。这是他们的主要特点。sys.builtin_module_names元组存储他们的名字:

$ python -q >>>import sys >>>sys.builtin_module_names ( '_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 交互。另一方面,它们不是可执行文件的一部分,而是在导入期间动态加载。包括array、math和在内的一些标准模块select是 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文件时,首先会.pyc在__pycache__目录中搜索对应的文件并执行。如果.pyc文件不存在,Python 会编译代码并创建文件。

$ ls module.pyc $ python module.pyc I'm a .pyc File $ python -c "import module" I'm a .pyc file


Submodule和Package

如果模块名称仅限于简单的标识符,如mymodule或utils,那么它们都必须是唯一的,每次给新文件命名时,我们都必须非常认真地考虑。出于这个原因,Python 允许模块有子模块和模块名称包含点“.”符号。

当 Python 执行此语句时:

import a.b

它首先导入模块a,然后是子模块a.b。它将子模块添加到模块的字典中并将模块分配给变量a,因此我们可以将子模块作为模块的属性来访问。

可以有子模块的模块称为package。从技术上讲,包是具有__path__属性的模块。这个属性告诉 Python 在哪里寻找子模块。当 Python 导入顶级模块时,它会在 .ZIP 文件中列出的目录和 ZIP 存档中搜索该模块sys.path。但是当它导入一个子模块时,它使用__path__父模块的属性而不是sys.path.


常规package

目录是将模块组织成包的最常见方式。如果目录包含__init__.py文件,则认为它是regular package。当 Python 导入这样一个目录时,它会执行该__init__.py文件,因此在那里定义的名称成为模块的属性。

该__init__.py文件通常为空或包含与包相关的属性,例如__doc__和__version__。它还可以用于将包的公共 API 与其内部实现分离。假设一个具有以下结构的库:

mylibrary/ __init__.py module1.py module2.py

而你要给使用你的library用户提供两种功能:module1.py定义的func1()和module2.py定义的func2()。如果__init__.py留空,则用户必须指定子模块以导入函数:

from mylibrary.module1 import func1 from mylibrary.module2 import func2

但你可能还希望允许用户导入这样的函数:

from mylibrary import func1 func2

所以在__init__.py导入函数:

# mylibrary/__init__.py from mylibrary.module1 import func1 from mylibrary.module2 import func2

具有扩展名__init__.so的C扩展目录或者名__init__.pyc的.pyc文件也是一个regular package。Python 可以完美地导入这样的包:

$ ls spam $ ls spam/ __init__.so $ python -q >>> import spam >>>


命名空间package

在 3.3 版本之前,Python 只有常规package。没有_init__.py的目录根本不被视为包。因为人们不喜欢创建空__init__.py文件。PEP 420通过在 Python 3.3 中引入命名空间包无需强制这些文件。

命名空间包也解决了另一个问题。它们允许开发人员将包的内容放置在多个位置。例如,如果您有以下目录结构:

mylibs/ company_name/ package1/... morelibs/ company_name/ package2/...

mylibs和morelibs都在sys.path,那么你就可以同时导入package1和package2:

>>>import company_name.package1 >>>import company_name.package2

这是因为company_name是一个包含两个位置的命名空间包:

>>>company_name.__path__ _NamespacePath(['/morelibs/company_name' '/mylibs/company_name'])

当 Python在模块搜索期间遍历路径(sys.path或 parent 的__path__)中的路径条目时,它会记住__init__.py与模块名称不匹配的目录。如果遍历所有条目后,找不到常规包、Python 文件或 C 扩展名,它会创建一个__path__包含存储目录的模块对象。


from mudule import

除了导入模块,我们还可以使用from <> import <>语句导入模块属性,如下所示:

from module import func Class submodule

此语句导入一个模块并将指定的属性分配给相应的变量:

func = module.func Class = module.Class submodule = module.submodule

请注意,可以删除,然后变量不可用:

del module

当 Python 发现某个模块没有指定的属性时,它会将该属性视为子模块并尝试导入它。因此,如果module定义func和Class但不是submodule,Python 将尝试导入module.submodule.


通配符import

如果我们不想明确指定从模块导入的名称,我们可以使用导入的通配符形式:

from module import *

这条语句就像"*"被替换为所有模块的公共名称一样。这些是模块字典中不以下划线开头"_"的名称或__all__属性中列出的名称(如果已定义)。


相对import

到目前为止,我们一直通过指定绝对模块名称来告诉 Python 要导入哪些模块。该from <> import <>语句还允许我们指定相对模块名称。这里有一些例子:

from . import a from .. import a from .a import b from ..a.b import c

__package__模块的属性存储模块所属的包的名称。如果模块是一个包,则该模块属于它自己,并且 __package__是模块自己的名称 ( __name__)。如果模块是子模块,则它属于父模块,并__package__设置为父模块的名称。最后,如果模块既不是包也不是子模块,那么它的包是未定义的。在这种情况下,__package__可以设置为空字符串(例如模块是顶级模块)或None(例如模块作为脚本运行)。

相对模块名称是前面有一些点的模块名称。一个点代表当前包。因此,当__package__定义时,以下语句:

from . import a

就好像点被替换为 __package__。

如果你试试这个:

from ... import e

Python 会抛出一个错误:

ImportError: attempted relative import beyond top-level package

这是因为 Python 不会通过文件系统来解析相对导入。它只需要 __package__,去除一些后缀并附加一个新的后缀以获得绝对模块名称。

显然,相对导入在__package__根本没有定义时会中断。在这种情况下,您会收到以下错误:

ImportError: attempted relative import with no known parent package


将程序作为模块运行

在运行具有相对导入的程序时避免导入错误的标准方法是使用-m将其作为模块运行:

$ python -m package.module

该-m告诉 Python 使用与import相同的机制来查找模块。Python 获取模块名称并能够计算当前包。例如,如果我们运行一个名为 的模块package.module,其中module引用了一个常规.py文件,那么代码将在属性设置为的__main__模块中执行。


对导入语句进行脱皮

如果我们对任何import语句进行脱皮,我们将看到它最终会调用内置__import__()函数。该函数接受一个模块名称和一堆其他参数,找到该模块并为其返回一个模块对象。

Python 允许设置__import__()为自定义函数,因此我们可以完全改变import过程。例如,这是一个毁掉一切的更改:

>>> import builtins >>> builtins.__import__ = None >>> import math Traceback (most recent call last): File "<stdin>" line 1 in <module> TypeError: 'NoneType' object is not callable

__import__()的默认实现是importlib.__import__().。该importlib模块是一个标准的模块,是import系统的核心。它是用 Python 编写的,因为导入过程涉及路径处理和你更喜欢用 Python 而不是 C 来做。但importlib出于性能原因, 某些函数被移植到 C 中。


简单的import

一段 Python 代码分两步执行:

  1. 该编译器编译代码的字节码。
  2. 该虚拟机执行改字节码。

要看到一个import语句做什么的,大家可以看一下它产生的字节码,然后找出每个字节码指令。

为了获取字节码,我们使用dis标准模块:

$ 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。

执行IMPORT_NAME指令的代码如下所示:

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(level); Py_DECREF(fromlist); SET_TOP(res); if (res == NULL) goto error; DISPATCH(); }

所有的动作都发生在import_name()函数中。

import m

实际上相当于这段代码:

m = __import__ ( 'm' globals () locals () None 0 )

根据文档字符串的参数含义importlib.__import__()如下:

def __import__ ( name globals = None locals = None fromlist = () level = 0 ): """导入一个模块。 'globals' 参数用于推断导入发生的位置 以处理相对导入。'locals' 参数忽略。该 “fromlist里”参数指定应该怎样作为属性存在模块上 被导入(例如``从模块进口<fromlist里>``)。'level' 参数表示在相对导入中要从中导入的包位置 (例如“from ..pkg import mod” 的“级别”为 2)。 """

所有导入语句最终都会调用__import__(). 他们在方式有所不同。例如,相对导入传递非零level,from <> import <>语句传递非空fromlist。


Importing submodules

import a.b.c

编译为以下字节码:

$ echo "import a.b.c" | python -m dis 1 0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (None) 4 IMPORT_NAME 0 (a.b.c) 6 STORE_NAME 1 (a) ...

并等价于以下代码:

a = __import__('a.b.c' globals() locals() None 0)

参数以__import__()的方式传递。和import m唯一的区别是__import__()的不是模块的名称(a.b.c不是有效的变量名称),而是分配点之前的第一个标识符(a),即__import__()在这种情况下返回顶级模块。


from <> import <>

这个说法:

from a.b import f g

编译为以下字节码:

$ echo "from a.b import f g" | python -m dis 1 0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (('f' 'g')) 4 IMPORT_NAME 0 (a.b) 6 IMPORT_FROM 1 (f) 8 STORE_NAME 1 (f) 10 IMPORT_FROM 2 (g) 12 STORE_NAME 2 (g) 14 POP_TOP ...

并等价于以下代码:

a_b = __import__('a.b' globals() locals() ('f' 'g') 0) f = a_b.f g = a_b.g del a_b

要导入的名称使用fromlist传递. 当fromlist不为空时,__import__()返回的不是像简单导入那样的顶级模块,而是像a.b.


from <> import *

from m import *

编译为以下字节码:

$ echo "from m import *" | python -m dis 1 0 LOAD_CONST 0 (0) 2 LOAD_CONST 1 (('*' )) 4 IMPORT_NAME 0 (m) 6 IMPORT_STAR ...

并等价于以下代码:

m = __import__('m' globals() locals() ('*' ) 0) all_ = m.__dict__.get('__all__') if all_ is None: all_ = [k for k in m.__dict__.keys() if not k.startswith('_')] for name in all_: globals()[name] = getattr(m name) del m all_ name

该__all__属性列出了模块的所有公共名称。如果__all__未定义中列出的某些名称,则__import__()尝试将它们作为子模块导入。


相对import

from .. import f

编译为以下字节码

$ echo "from .. import f" | python -m dis 1 0 LOAD_CONST 0 (2) 2 LOAD_CONST 1 (('f' )) 4 IMPORT_NAME 0 6 IMPORT_FROM 1 (f) 8 STORE_NAME 1 (f) 10 POP_TOP ...

并等价于以下代码:

m = __import__('' globals() locals() ('f' ) 2) f = m.f del m

该参数2告诉__import__()相对导入有多少个前导点。


__import__() 做了什么

__import__()实现的算法总结如下:

  1. 如果level > 0,则将相对模块名称解析为绝对模块名称。
  2. 导入模块。
  3. 如果fromlist为空,则删除模块名称中第一个点之后的所有内容以获取顶级模块的名称。导入并返回顶级模块。
  4. 如果fromlist包含不在模块字典中的名称,请将它们作为子模块导入。也就是说,如果submodule不在模块的字典中,则导入module.submodule。如果"*" 在 中fromlist,则使用模块__all__作为新的fromlist并重复此步骤。
  5. 返回模块。

import流程

该_find_and_load()函数采用绝对模块名称并执行以下步骤:

  1. 如果模块在 中sys.modules,则返回它。
  2. 将模块搜索路径初始化为None.
  3. 如果模块有一个父模块(名称至少包含一个点),如果它还没有,请导入父模块sys.modules。将模块搜索路径设置为 parent 的__path__.
  4. 使用模块名称和模块搜索路径查找模块的规范。如果未找到规范,则提出ModuleNotFoundError.
  5. 从规范加载模块。
  6. 将模块添加到父模块的字典中。
  7. 返回模块。

所有导入的模块都存储在sys.modules字典中。该字典将模块名称映射到模块对象并充当缓存。在搜索模块之前,如果它在那里,立即_find_and_load()检查sys.modules并返回模块。导入的模块会sys.module在步骤 5 的末尾添加。

如果模块不在 中sys.modules,则_find_and_load() 继续导入过程。此过程包括查找模块和加载模块。Finders 和 loader 是执行这些任务的对象。


导入流程总结

任何 import 语句都编译为一系列字节码指令,其中一个称为IMPORT_NAME,通过调用内置__import__()函数导入模块。如果模块是用相对名称指定的,则__import__()首先使用__package__当前模块的属性将相对名称解析为绝对名称。然后它查找sys.modules的模块并返回模块。如果模块不存在,则__import__()尝试查找模块的规范。它调用find_spec()列出的每个查找程序的方法,sys.meta_path直到某个查找程序返回规范。如果模块是内置模块,则BuiltinImporter返回规范。如果模块是冻结模块,则FrozenImporter返回规范。否则,PathFinder在模块搜索路径上搜索模块,即__path__父模块的属性,或者sys.path。PathFinder 迭代路径条目,并为每个条目调用find_spec()相应路径条目查找器的方法。要获取相应的路径条目查找器,PathFinder请将路径条目传递给sys.path_hooks. 如果路径条目是目录的路径,则可调用对象之一返回一个FileFinder在该目录中搜索模块的实例。PathFinder称其find_spec(). 所述find_spec()的方法FileFinder 检查由路径条目中指定的目录中包含一个C扩展,一个.py文件,一个.pyc文件或目录的名字的模块名称相匹配。如果它找到任何东西,它会使用相应的加载器创建一个模块规范。当__import__()获取规范时,它调用加载器的create_module()方法来创建模块对象,然后调用exec_module()来执行模块。最后,它将模块放入sys.modules并返回模块。

猜您喜欢: