优秀的编程知识分享平台

网站首页 > 技术文章 正文

Python 幕后:Python导入import的工作原理

nanyue 2024-10-10 07:30:15 技术文章 6 ℃

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

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并返回模块。

最近发表
标签列表