Python 幕后:Python导入import的工作原理
wptr33 2025-07-10 21:26 29 浏览
更多互联网精彩资讯、工作效率提升关注【飞鱼在浪屿】(日更新)
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 将以下内容识别为模块:
- 内置modules。
- 冻结modules。
- C 扩展。
- Python 源代码文件(.py文件)。
- Python 字节码文件(.pyc文件)。
- 目录。
内置模块是编译成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 代码分两步执行:
- 该编译器编译代码的字节码。
- 该虚拟机执行改字节码。
要看到一个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__()实现的算法总结如下:
- 如果level > 0,则将相对模块名称解析为绝对模块名称。
- 导入模块。
- 如果fromlist为空,则删除模块名称中第一个点之后的所有内容以获取顶级模块的名称。导入并返回顶级模块。
- 如果fromlist包含不在模块字典中的名称,请将它们作为子模块导入。也就是说,如果submodule不在模块的字典中,则导入module.submodule。如果"*" 在 中fromlist,则使用模块__all__作为新的fromlist并重复此步骤。
- 返回模块。
import流程
该_find_and_load()函数采用绝对模块名称并执行以下步骤:
- 如果模块在 中sys.modules,则返回它。
- 将模块搜索路径初始化为None.
- 如果模块有一个父模块(名称至少包含一个点),如果它还没有,请导入父模块sys.modules。将模块搜索路径设置为 parent 的__path__.
- 使用模块名称和模块搜索路径查找模块的规范。如果未找到规范,则提出ModuleNotFoundError.
- 从规范加载模块。
- 将模块添加到父模块的字典中。
- 返回模块。
所有导入的模块都存储在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并返回模块。
相关推荐
- MySQL进阶五之自动读写分离mysql-proxy
-
自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...
- 3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?
-
引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...
- 一文由浅入深带你完全掌握MySQL的锁机制原理与应用
-
本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...
- 验证Mysql中联合索引的最左匹配原则
-
后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...
- MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)
-
目录1.索引基础...
- 你会看 MySQL 的执行计划(EXPLAIN)吗?
-
SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...
- MySQL 从入门到精通(四)之索引结构
-
索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...
- mysql总结——面试中最常问到的知识点
-
mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...
- mysql总结——面试中最常问到的知识点(2)
-
首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...
- MySQL基础全知全解!超详细无废话!轻松上手~
-
本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...
- 深入剖析 MySQL 中的锁机制原理_mysql 锁详解
-
在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...
- Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析
-
引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...
- MySQL基础篇:DQL数据查询操作_mysql 查
-
一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...
- MySql:索引的基本使用_mysql索引的使用和原理
-
一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
Java面试必考问题:什么是乐观锁与悲观锁
-
如何将AI助手接入微信(打开ai手机助手)
-
redission YYDS spring boot redission 使用
-
SparkSQL——DataFrame的创建与使用
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)