对于每一位Python开发者来说,import
这个关键字是再相熟不过了,无论是咱们援用官网库还是三方库,都能够通过import xxx
的模式来导入。可能很多人认为这只是Python的一个最根底的常识之一,仿佛没有能够扩大的点了,确实,它是Python体系中的根底,然而往往“最根底的也最重要”,设想下当被面试官要求谈谈“Python Import System”的时候,你真的能够娓娓而谈聊上一个小时吗?
关注公众号《技术拆解官》,回复“import”获取高清PDF浏览版本
其实真的能够做到。不过既然要开始聊,那咱们先得在缕清“Python Import System”的整体流程是什么样的
一、根底概念
1. 什么能够被import?– Python中的基本概念
在介绍Import System
之前,咱们要理解的是在Python中什么能够被import。
这个问题如同不难答复,因为Python中一切都是对象,都是同属于object,也是任何货色都是能够被import的,在这些不同的对象中,咱们常常应用到的也是最重要的要算是模块(Module) 和 包(Package) 这两个概念了,不过尽管外表上看去它们是两个概念,然而在Python的底层都是PyModuleObject
构造体实例,类型为PyModule_Type
,而在Python中则都是体现为一个<class 'module'>
对象。
// Objects/moduleobject.c
PyTypeObject PyModule_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"module", /* tp_name */
sizeof(PyModuleObject), /* tp_basicsize */
// ...
};
// Python中的<class 'module'>对应底层的PyModule_Type
// 而导入进来的模块对象 则对应底层的 PyModuleObject
咱们再来看看在Python中导入模块和包之后的体现
import os
import pandas
print(os) # <module 'os' from 'C:\\python38\\lib\\os.py'>
print(pandas) # <module 'pandas' from 'C:\\python38\\lib\\site-packages\\pandas\\__init__.py'>
print(type(os)) # <class 'module'>
print(type(pandas)) # <class 'module'>
从下面的后果能够看进去,不论是模块还是包,在Python中都是一样的,它们都是一个PyModuleObject
,在Python的底层没有辨别那么显著,不过在为了便于咱们的开发,咱们通常是这么来辨别它们的:
1.1 模块
Python中,不论是对常见的*.py
文件,或是编译优化的*.pyc
, *.pyo
文件、扩大类型的*.pyd
,*.pyw
文件来说, 它们是属于Python代码载体的最小单元,这样独自存在的文件咱们都称之为“模块”。
1.2 包
上述这样的多个模块组合在一起,咱们就称之为“包”,而Python中,大家比拟相熟的包类型是蕴含__init__.py
的包。通常来说,咱们习惯创立包的步骤基本上都是新建目录A、新建__init__.py
,再之后咱们就能够欢快地导入包A了。然而,这也只是包的一种类型—“Regular packages”,事实上,在PEP 420 — Implicit Namespace Packages中提到,从Python3.3版本开始引入了“Namespace Packages”这个新的包类型,这种包类型和之前提到的一般包类型的区别如下:
- 要害区别:不蕴含
__init__.py
,所以被会辨认成Namespace Packages - 当然也不会有
__file__
属性,因为对于一般的packages来说__file__
属性指定__init__.py
的地址 __path__
不是个List,而变成了是只读可迭代属性,当批改父门路(或者最高层级包的sys.path)的时候,属性会自动更新,会在该包内的下一次导入尝试时主动执行新的对包局部的搜寻__loader__
属性中能够蕴含不同类型的对象,也就是能够通过不同类型的loader加载器来加载- 包中的子包能够来自不同目录、zip文件等所以能够通过Python
find_spec
搜寻到的中央,同上,波及到import的原理
额定讲讲对于“Namespace Packages”的益处,不是为了导入没有__init__.py
的包,而是想要利用Python的import
机制来保护一个虚拟空间,更好的组织不同目录下的子包以及对于子包的选择性应用,让它们可能对立的被大的命名空间所治理。
比方咱们有这样的构造
└── project
├── foo-package
│ └── spam
│ └── blah.py
└── bar-package
└── spam
└── grok.py
在这2个目录里,都有着独特的命名空间spam。在任何一个目录里都没有__init__.py
文件。
让咱们看看,如果将foo-package和bar-package都加到python模块门路并尝试导入会产生什么
>>> import sys
>>> sys.path.extend(['foo-package', 'bar-package'])
>>> import spam.blah # 失常导入
>>> import spam.grok # 同样也是失常导入
>>>
两个不同的包目录被合并到一起,你能够选择性的导入spam.blah和spam.grok或是其余有限扩大的包,它们曾经造成了一个“Namespace Packages”,能够失常工作。
更多的概念能够去具体看下PEP 420 — Implicit Namespace Packages,摸索更多的玩法。
2. import的形式?– 相对导入/绝对导入
理解完“什么能够被import”之后,接下来讲讲对于import的形式有哪些?对于Python 2.X与Python 3.X的导入机制有较大的差异,次要体现在两个工夫节点上:
- PEP 420 — Implicit Namespace Packages — Python 3.3之后引入的“Namespace Packages”
- PEP 328 — Imports: Multi-Line and Absolute/Relative–Absolute/Relative Package
对于第一点咱们之前曾经谈过了,当初重点谈谈第二点,有就是有对于相对导入与绝对导入的问题。
在Python 2.6之前,Python的默认import机制是“relative import(绝对导入)”,而之后则改成了“absolute import(相对导入)”,那这两个不同的导入机制该怎么了解呢?
首先无论是相对导入还是绝对导入,都须要一个参照物,不然“相对”与“绝对”的概念就无从谈起,相对导入的参照物是我的项目的根文件夹,而绝对导入的参照物为以后地位,上面咱们通过一个例子来解释下这两个机制:
首先咱们创立好这样的目录构造:
└── project
├── package1
│ ├── module1.py
│ └── module2.py
│ └── Function Fx
└── package2
├── __init__.py
│ └── Class Cx
├── module3.py
├── module4.py
└── subpackage1
└── module5.py
└── Function Fy
2.1 相对导入
绝对路径要求咱们必须从最顶层的文件夹开始,为每个包或每个模块提供出残缺具体的导入门路
比方咱们想要导入相干的类或者函数的话就要这样
from package1 import mudule1
from package1.module2 import Fx
from package2 import Cx
from package2.subpackage1.module5 import Fy
劣势:
- 代码档次清晰:能够很清晰的理解每条导入数据的全门路,不便咱们及时找到具体引入地位。
- 打消绝对地位依赖的问题:能够很不便的执行独自的py文件,而不必思考援用出错、绝对地位依赖等等问题。
劣势:
- 顶层包名硬编码:这种形式对于重构代码来说将会变得很简单,你须要查看所有文件来修复硬编码的门路(当然,应用IDE能够疾速更改),另一方面如果你想挪动代码或是别人须要应用你的代码也是很麻烦的事件(因为波及从根目录开始写门路,当作为另一个我的项目的子模块时,又须要扭转整个我的项目的包名构造)PS:当然能够通过打包解决。
- 导入包名过长:当我的项目层级过于宏大时,从根目录导入包会变得很简单,不仅须要理解整个我的项目的逻辑、包构造,而且在包援用多的时候,频繁的写门路会变得让人很焦躁。
2.2 绝对导入
当咱们应用绝对导入时,须要给出绝对与以后地位,想导入资源所在的地位。
绝对导入分为“隐式绝对导入”和“显式绝对导入”两种,比方咱们想在package2/module3.py中援用module4模块,咱们能够这么写
# package2/module3.py
import module4 # 隐式绝对导入
from . import module4 # 显式绝对导入
from package2 import module4 # 相对导入
想在package2/module3.py中导入class Cx和function Fy,能够这么写
# package2/module3.py
import Cx # 隐式绝对导入
from . import Cx # 显式绝对导入
from .subpackage1.module5 import Fy
代码中.
示意以后文件所在的目录,如果是..
就示意该目录的上一层目录,三个.
、四个.
顺次类推。能够看出,隐式绝对导入相比于显式绝对导入无非就是隐含了当前目录这个条件,不过这样会容易引起凌乱,所以在PEP 328的时候被正式淘汰,毕竟“Explicit is better than implicit”。
劣势:
- 援用简洁:和相对导入相比,援用时能够依据包的绝对地位引入,不必理解整体的我的项目构造,更不必写简短的绝对路径,比方咱们能够把
from a.b.c.d.e.f.e import e1
变成from . import e1
劣势:
- 应用困扰:相比于绝对路径,相对路径因为须要更加明确绝对地位,因而在应用过程中常会呈现各种各样的问题,比方上面这些案例
假如咱们把我的项目构造改成这样
└── project
├── run.py
├── package1
│ ├── module1.py
│ └── module2.py
│ └── Function Fx
└── package2
├── __init__.py
│ └── Class Cx
├── module3.py
├── module4.py
└── subpackage1
└── module5.py
└── Function Fy
2.2.1 top-level package辨别问题
对于执行入口run.py,咱们这么援用
from package2 import module3
from package2.subpackage1 import module5
对于module3.py、module5.py别离批改成这样
# package2/module3
from ..package1 import module2
# package2/subpackage1/module5
from .. import module3
def Fy():
...
此时,执行python run.py会造成这样的谬误
Traceback (most recent call last):
File "run.py", line 1, in <module>
from package2 import module3
File "G:\company_project\config\package2\module3.py", line 1, in <module>
from ..package1 import module2
# 试图在顶级包(top-level package)之外进行绝对导入
ValueError: attempted relative import beyond top-level package
起因就在于当咱们把run.py当成执行模块时,和该模块同级的package1和package2被视为顶级包(top-level package),而咱们跨顶级包来援用就会造成这样的谬误。
2.2.2 parent package异样问题
对于执行入口run.py的援用,咱们批改成这样
from .package1 import module2
此时,执行python run.py会造成这样的谬误
Traceback (most recent call last):
File "run.py", line 1, in <module>
from .package1 import module2
# 没有找到父级包
ImportError: attempted relative import with no known parent package
为什么会这样呢?依据PEP 328的解释
Relative imports use a module’s name attribute to determine that module’s position in the package hierarchy. If the module’s name does not contain any package information (e.g. it is set to main ) then relative imports are resolved as if the module were a top level module, regardless of where the module is actually located on the file system.
绝对导入通过应用模块的__name__属性来确定模块在包层次结构中的地位。如果该模块的名称不蕴含任何包信息(例如,它被设置为__main__),那么绝对援用会认为这个模块就是顶级模块,而不论模块在文件系统上的理论地位。
换句话说,绝对导入寻找绝对地位的算法是基于__name__
和__package__
变量的值。大部分时候,这些变量不蕴含任何包信息。比方:当__name__=__main__,__package__=None
时,Python解释器不晓得模块所属的包。在这种状况下,绝对援用会认为这个模块就是顶级模块,而不论模块在文件系统上的理论地位。
咱们看看run.py的__name__和__package__值
print(__name__,__package__)
# from .package1 import module2
后果是
__main__ None
正如咱们所看到的,Python解释器认为它就是顶级模块,没有对于模块所属的父级包的任何信息(__name__=__main__,__package__=None
),因而它抛出了找不到父级包的异样。
3. 规范化import
对于团队开发来说,代码规范化是很重要的,因而在理解如何将包、模块import之后,对于import的种种标准的理解,也是必不可少的。
3.1 import写法
对于import的写法的,参照官网代码标准文件PEP 8的阐明
- Imports should usually be on separate lines(不同包分行写)
- Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants(地位在文件的顶部,就在任何模块正文和文档字符串之后,模块全局变量和常量之前)
- Absolute imports are recommended, as they are usually more readable and tend to be better behaved (or at least give better error messages) if the import system is incorrectly configured (such as when a directory inside a package ends up on sys.path)(举荐相对导入,波及到我的项目构造,前面会提到)
- Wildcard imports (from <module> import *) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools(尽量应用通配符导入)
Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants.
Imports should be grouped in the following order:
- Standard library imports.
- Related third party imports.
- Local application/library specific imports.
You should put a blank line between each group of imports. (不同类别的包导入程序、以及距离为一行)
以及PEP 328的
Rationale for Parentheses
- Instead, it should be possible to use Python’s standard grouping mechanism (parentheses) to write the import statement:
依据下面的标准,咱们照样举个案例
# 多行拆分
# 倡议
import os
import sys
# 同包容许不分
from subprocess import Popen, PIPE
# 不倡议
import os,sys
# 文件顶部
# 正文....
import os
a = 1
# 导入你须要的,不要净化local空间
# 倡议
from sys import copyright
# 不倡议
from sys import *
# 包导入程序
import sys # 零碎库
import flask # 三方库
import my # 自定义库
# 利用好python的规范括号分组
# 倡议
from sys import (copyright, path, modules)
# 不倡议
from sys import copyright, path, \
modules
尽管这些不是强求的标准写法,然而对于工程师来说,代码规范化总是会让人感觉“特地优良”。
3.2 我的项目构造
下面剖析了两种不同导入形式的优缺点,那么如何在理论我的项目中去更好的对立咱们我的项目的import的构造呢?当然,尽管官网举荐的导入形式是相对导入,然而咱们也是要用批评的眼光去对待。举荐大家去看一些优良开源库的代码,比方torando
以及fastapi
# fastapi\applications.py
from fastapi import routing
from fastapi.concurrency import AsyncExitStack
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from fastapi.logger import logger
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
# tornado\gen.py
from tornado.concurrent import (
Future,
is_future,
chain_future,
future_set_exc_info,
future_add_done_callback,
future_set_result_unless_cancelled,
)
from tornado.ioloop import IOLoop
from tornado.log import app_log
from tornado.util import TimeoutError
能够看到,目前很多的开源库都是将本人的导入形式往相对导入的方向转变,也因为他们的单个库的层次结构不深,因而并没有受到简单档次嵌套的影响,也有人会问,那对于特地宏大的大型项目来说,如何解决多层嵌套的问题?这个问题就须要联合具体的我的项目来剖析,是拆分大型项目成一个个小型子项目还是采纳“相对导入+绝对导入”相互联合的形式都是须要结合实际的场景去思考的,对于小型我的项目来说,相似上述两个开源我的项目这样,“相对导入+打包”的形式既利于源码浏览有便于我的项目应用、移植,是最举荐的计划。
二、外围import机制
第一局部提到的都是些根底概念性的内容,大部分也或多或少会在日常开发中接触到,然而其实相似于“import如何找到包?”、“import如何加载到包?”、“import底层的解决流程是什么?”等等这些问题对于很多开发者是很少去接触的,咱们没有了解Import System的外围解决逻辑,是很难更好的应用它的,也就是进行批改和二次开发,另一方面,对于咱们后续的大型零碎架构的设计也是有肯定影响,因而,这个局部咱们一起来梳理下整个Import System中的外围import机制。
上面的内容咱们着重从源码动手来聊聊Import System中外围的import机制
1. import关键字做了什么工作? — 理清import关键字的逻辑
对于个别的开发者来说,最相熟的就是import
关键字了,那咱们就从这个关键字动手,第一步对于import的了解咱们从官网文档动手,也就是这篇Python参考手册第五章的《The import system》,文档的最开始就强调了
Python code in one module gains access to the code in another module by the process of importing it. The import statement is the most common way of invoking the import machinery, but it is not the only way. Functions such as importlib.import_module() and built-in __import__() can also be used to invoke the import machinery.
The import statement combines two operations; it searches for the named module, then it binds the results of that search to a name in the local scope. The search operation of the import statement is defined as a call to the __import__() function, with the appropriate arguments. The return value of __import__() is used to perform the name binding operation of the import statement. See the import statement for the exact details of that name binding operation.
A direct call to __import__() performs only the module search and, if found, the module creation operation. While certain side-effects may occur, such as the importing of parent packages, and the updating of various caches (including sys.modules), only the import statement performs a name binding operation.
When an import statement is executed, the standard builtin __import__() function is called. Other mechanisms for invoking the import system (such as importlib.import_module()) may choose to bypass __import__() and use their own solutions to implement import semantics.
咱们能从这个介绍中失去这几个信息:
首先,这段话先通知了咱们能够通过三种形式来导入模块,并且应用这三种形式的作用是雷同的
# import 关键字形式
import os
print(os) # <module 'os' from 'C:\\python38\\lib\\os.py'>
# import_module 规范库形式
from importlib import import_module
os = import_module("os")
print(os) # <module 'os' from 'C:\\python38\\lib\\os.py'>
# __import__ 内置函数形式
os = __import__("os")
print(os) # <module 'os' from 'C:\\python38\\lib\\os.py'>
接着是调用关键字import
的时候次要会触发两个操作:搜寻和加载(也能够了解为搜寻模块在哪里和把找到的模块加载到某个中央),其中搜寻操作会调用__import__
函数失去具体的值再由加载操作绑定到以后作用域,也就是说import关键字底层调用了__import__内置函数。另外须要留神的是,__import__
尽管只作用在模块搜寻阶段,然而会有额定的副作用,另一方面,__import__
尽管是底层的导入机制,然而import_module
可能是应用的本人的一套导入机制。
从介绍中咱们大略能了解关键字import
的某些信息,不过具体的还是要深刻源码来看,首先咱们先看关键字import
的源码逻辑,以一个最简略的形式来看import的底层调用,首先创立一个文件,为了不受其余因素影响,只有一行简略的代码
# test.py
import os
因为import是Python关键字,也就没方法通过IDE来查看它的源码,那就换个思路,间接查看它的字节码,Python中查看字节码的形式是通过dis模块来实现的,能够间接通过-m dis
或是import dis
来操作字节码,为了保障字节码的整洁,咱们这么操作
python -m dis test.py
失去如下的后果
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (os)
6 STORE_NAME 0 (os)
8 LOAD_CONST 1 (None)
10 RETURN_VALUE
能够发现,import os
代码对应的字节码真的是很简短,咱们先不论结尾的两个LOAD_CONST
压栈指令(不晓得具体作用,空的文件也会呈现),往下会发现IMPORT_NAME
、STORE_NAME
这两个指令,它们都是以os为参数调用,猜想应该是IMPORT_NAME
指令是把os这个module导入进来,再调用STORE_NAME
指令把这个导入的module保留在以后作用域的local空间内,期待咱们调用os的办法时,就能够依据os字符找到对应的module了,这个是Python解析器执行import字节码的解决流程
咱们的重点还是放在IMPORT_NAME
这个指令上,咱们去看看它的具体实现,Python的指令集都是集中在ceval.c
中实现的,所以咱们就跟踪进去寻找IMPORT_NAME
指令。
PS:对于怎么查看C代码的话,置信各位大佬都有本人的办法,我这里就应用Understand和大家一起看看,工具老手,没用上高级性能,大家见笑。
ceval.c
中有一个对于指令抉择的很宏大的switch case,而IMPORT_NAME
指令的case如下:
// ceval.c
case TARGET(IMPORT_NAME): {
// 获取模块名
PyObject *name = GETITEM(names, oparg);
// 猜想对应的是之前的LOAD_CONST压栈指令,这里把值取出,也就是之前的0和None,别离赋予给了level和fromlist
// 这两个值待解释
PyObject *fromlist = POP();
PyObject *level = TOP();
// 初始化模块对象PyModuleObject *
PyObject *res;
// import重点办法,调用import_name,将返回值返回给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
这个函数,它的参数tstate, f, name, fromlist, level
先记住,持续往下看
// ceval.c
// 调用
res = import_name(tstate, f, name, fromlist, level);
import_name(PyThreadState *tstate, PyFrameObject *f,
PyObject *name, PyObject *fromlist, PyObject *level)
{
_Py_IDENTIFIER(__import__);
PyObject *import_func, *res;
PyObject* stack[5];
// 获取__import__函数,能够看到的确如官网所说
// import的底层调用了__import__这个内置函数
import_func = _PyDict_GetItemIdWithError(f->f_builtins, &PyId___import__);
// 为NULL示意获取失败, 在Python解释器中会产生__import__ not found谬误
// 之后再通过某种机制失去相似模块未找到的谬误
if (import_func == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyErr_SetString(tstate, PyExc_ImportError, "__import__ not found");
}
return NULL;
}
/* Fast path for not overloaded __import__. */
// 判断__import__是否被重载了,tstate来自参数
if (import_func == tstate->interp->import_func) {
// import本人的原生门路,当__import__还未被重载的时候应用
int ilevel = _PyLong_AsInt(level);
if (ilevel == -1 && _PyErr_Occurred(tstate)) {
return NULL;
}
//未重载的话,调用PyImport_ImportModuleLevelObject
res = PyImport_ImportModuleLevelObject(
name,
f->f_globals,
f->f_locals == NULL ? Py_None : f->f_locals,
fromlist,
ilevel);
return res;
}
Py_INCREF(import_func);
// __import__的门路,结构栈
stack[0] = name;
stack[1] = f->f_globals;
stack[2] = f->f_locals == NULL ? Py_None : f->f_locals;
stack[3] = fromlist;
stack[4] = level;
// 调用__import__
res = _PyObject_FastCall(import_func, stack, 5);
Py_DECREF(import_func);
return res;
}
依据代码所示,import_name
其实是有两种门路的,也就是并不是import关键字底层并不是齐全调用__import__
函数的
- import关键字原生导入门路
__import__
内置函数导入门路
这里留神下对于__import__
函数的调用,传入了方才咱们没有解释的参数fromlist
和level
,咱们能够通过查看__import__
函数源码来剖析这两个参数
def __import__(name, globals=None, locals=None, fromlist=(), level=0):
"""
__import__(name, globals=None, locals=None, fromlist=(), level=0) -> module
Import a module. Because this function is meant for use by the Python
interpreter and not for general use, it is better to use
importlib.import_module() to programmatically import a module.
The globals argument is only used to determine the context;
they are not modified. The locals argument is unused. The fromlist
should be a list of names to emulate ``from name import ...'', or an
empty list to emulate ``import name''.
When importing a module from a package, note that __import__('A.B', ...)
returns package A when fromlist is empty, but its submodule B when
fromlist is not empty. The level argument is used to determine whether to
perform absolute or relative imports: 0 is absolute, while a positive number
is the number of parent directories to search relative to the current module.
"""
pass
解释中表白了对于fromlist
参数来说,为空则导入顶层的模块,不为空则能够导入上层的值,例如
m1 = __import__("os.path")
print(m1) # <module 'os' from 'C:\\python38\\lib\\os.py'>
m2 = __import__("os.path", fromlist=[""])
print(m2) # <module 'ntpath' from 'C:\\python38\\lib\\ntpath.py'>
而另一个参数level
的含意是如果是0,那么示意仅执行相对导入,如果是一个正整数,示意要搜寻的父目录的数量。个别这个值也不须要传递。
解释完这两个参数之后,咱们接着往下剖析,首先剖析import关键字原生导入门路
1.1 import关键字原生导入门路
先来关注下import原生的门路,重点在PyImport_ImportModuleLevelObject
上
// Python/import.c
// 调用
res = PyImport_ImportModuleLevelObject(
name,
f->f_globals,
f->f_locals == NULL ? Py_None : f->f_locals,
fromlist,
ilevel)
PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level)
{
_Py_IDENTIFIER(_handle_fromlist);
PyObject *abs_name = NULL;
PyObject *final_mod = NULL;
PyObject *mod = NULL;
PyObject *package = NULL;
PyInterpreterState *interp = _PyInterpreterState_GET_UNSAFE();
int has_from;
// 非空查看
if (name == NULL) {
PyErr_SetString(PyExc_ValueError, "Empty module name");
goto error;
}
// 类型查看,是否合乎PyUnicodeObject
if (!PyUnicode_Check(name)) {
PyErr_SetString(PyExc_TypeError, "module name must be a string");
goto error;
}
// level不能够小于0
if (level < 0) {
PyErr_SetString(PyExc_ValueError, "level must be >= 0");
goto error;
}
// level大于0
if (level > 0) {
// 找绝对路径
abs_name = resolve_name(name, globals, level);
if (abs_name == NULL)
goto error;
}
else {
// 表明level==0
if (PyUnicode_GET_LENGTH(name) == 0) {
PyErr_SetString(PyExc_ValueError, "Empty module name");
goto error;
}
// 此时间接将name赋值给abs_name,因为此时是相对导入
abs_name = name;
Py_INCREF(abs_name);
}
// 调用PyImport_GetModule获取module对象
// 这个module对象会先判断是否存在在sys.modules外面
// 如果没有,那么才从硬盘上加载。加载之后在保留在sys.modules
// 在下一次导入的时候,间接从sys.modules中获取,具体细节前面聊
mod = PyImport_GetModule(abs_name);
//...
if (mod == NULL && PyErr_Occurred()) {
goto error;
}
//...
if (mod != NULL && mod != Py_None) {
_Py_IDENTIFIER(__spec__);
_Py_IDENTIFIER(_lock_unlock_module);
PyObject *spec;
/* Optimization: only call _bootstrap._lock_unlock_module() if
__spec__._initializing is true.
NOTE: because of this, initializing must be set *before*
stuffing the new module in sys.modules.
*/
spec = _PyObject_GetAttrId(mod, &PyId___spec__);
if (_PyModuleSpec_IsInitializing(spec)) {
PyObject *value = _PyObject_CallMethodIdObjArgs(interp->importlib,
&PyId__lock_unlock_module, abs_name,
NULL);
if (value == NULL) {
Py_DECREF(spec);
goto error;
}
Py_DECREF(value);
}
Py_XDECREF(spec);
}
else {
Py_XDECREF(mod);
// 要害局部代码
mod = import_find_and_load(abs_name);
if (mod == NULL) {
goto error;
}
}
//...
else {
// 调用importlib包中的公有_handle_fromlist函数来获取模块
final_mod = _PyObject_CallMethodIdObjArgs(interp->importlib,
&PyId__handle_fromlist, mod,
fromlist, interp->import_func,
NULL);
}
error:
Py_XDECREF(abs_name);
Py_XDECREF(mod);
Py_XDECREF(package);
if (final_mod == NULL)
remove_importlib_frames();
return final_mod;
}
PyImport_ImportModuleLevelObject
的代码中很多局部都有调用importlib
包的痕迹,看来importlib
在这个import
体系中占了很多比重,重点看一下import_find_and_load
函数
// Python/import.c
static PyObject *
import_find_and_load(PyObject *abs_name)
{
_Py_IDENTIFIER(_find_and_load);
PyObject *mod = NULL;
PyInterpreterState *interp = _PyInterpreterState_GET_UNSAFE();
int import_time = interp->config.import_time;
static int import_level;
static _PyTime_t accumulated;
_PyTime_t t1 = 0, accumulated_copy = accumulated;
PyObject *sys_path = PySys_GetObject("path");
PyObject *sys_meta_path = PySys_GetObject("meta_path");
PyObject *sys_path_hooks = PySys_GetObject("path_hooks");
if (PySys_Audit("import", "OOOOO",
abs_name, Py_None, sys_path ? sys_path : Py_None,
sys_meta_path ? sys_meta_path : Py_None,
sys_path_hooks ? sys_path_hooks : Py_None) < 0) {
return NULL;
}
/* XOptions is initialized after first some imports.
* So we can't have negative cache before completed initialization.
* Anyway, importlib._find_and_load is much slower than
* _PyDict_GetItemIdWithError().
*/
if (import_time) {
static int header = 1;
if (header) {
fputs("import time: self [us] | cumulative | imported package\n",
stderr);
header = 0;
}
import_level++;
t1 = _PyTime_GetPerfCounter();
accumulated = 0;
}
if (PyDTrace_IMPORT_FIND_LOAD_START_ENABLED())
PyDTrace_IMPORT_FIND_LOAD_START(PyUnicode_AsUTF8(abs_name));
// 调用了importlib的公有函数_find_and_load
mod = _PyObject_CallMethodIdObjArgs(interp->importlib,
&PyId__find_and_load, abs_name,
interp->import_func, NULL);
//...
return mod;
}
能够看到,导入的逻辑最初居然回到了importlib
包中,既然曾经回到了Python代码,那咱们先对于C代码的局部总结下,不过咱们仿佛还有另一个门路没看呢?
1.2 __import__
内置函数导入门路
还记得一开始import_name
的另一条门路吗?既然原生门路当初回到了Python局部,那么另一条__import__
也是可能和会原生门路在某个节点会合,再独特调用Python代码局部,咱们来看看对于__import__
函数的源码,因为__import__
是内置函数,代码在Python\bltinmodule.c
当中
// Python\bltinmodule.c
static PyObject *
builtin___import__(PyObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"name", "globals", "locals", "fromlist",
"level", 0};
PyObject *name, *globals = NULL, *locals = NULL, *fromlist = NULL;
int level = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "U|OOOi:__import__",
kwlist, &name, &globals, &locals, &fromlist, &level))
return NULL;
return PyImport_ImportModuleLevelObject(name, globals, locals,
fromlist, level);
}
很显著,独特调用了PyImport_ImportModuleLevelObject
函数,那咱们能够了解为当__import__
函数被重载时,原生门路就间接走PyImport_ImportModuleLevelObject
函数流程了。先来总结到目前为止C代码的调用流程
之前咱们始终在看CPython源码,当初要把眼光转化Python了,刚刚提到了当在sys.module
中未发现缓存模块时,就须要调用importlib
包的_find_and_load
的办法,从字面含意上猜想是对于模块搜寻和导入的,回忆之前在import
的解释中也提到了它次要分为了搜寻和加载两个过程, 那么_find_and_load
应该就是要开始剖析这两个过程了,而这两个过程别离对应的概念是Finder(查找器)
和Loader(加载器)
,具体的概念咱们在源码中再来解释,先来看源码
# importlib/_bootstrap.py
_NEEDS_LOADING = object()
def _find_and_load(name, import_):
"""Find and load the module."""
# 加了多线程锁的管理器
with _ModuleLockManager(name):
# 又在sys.modules中寻找了一遍,没有模块就调用_find_and_load_unlocked函数
module = sys.modules.get(name, _NEEDS_LOADING)
if module is _NEEDS_LOADING:
# name是绝对路径名称
return _find_and_load_unlocked(name, import_)
if module is None:
message = ('import of {} halted; '
'None in sys.modules'.format(name))
raise ModuleNotFoundError(message, name=name)
_lock_unlock_module(name)
return module
def _find_and_load_unlocked(name, import_):
path = None
# 拆解绝对路径
parent = name.rpartition('.')[0]
if parent:
if parent not in sys.modules:
_call_with_frames_removed(import_, parent)
# Crazy side-effects!
# 再次寻找
if name in sys.modules:
return sys.modules[name]
parent_module = sys.modules[parent]
try:
# 拿父类模块的__path__属性
path = parent_module.__path__
except AttributeError:
msg = (_ERR_MSG + '; {!r} is not a package').format(name, parent)
raise ModuleNotFoundError(msg, name=name) from None
# 这里体现出搜寻的操作了
spec = _find_spec(name, path)
if spec is None:
raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
else:
# 这里体现出加载的操作了
module = _load_unlocked(spec)
if parent:
# Set the module as an attribute on its parent.
parent_module = sys.modules[parent]
setattr(parent_module, name.rpartition('.')[2], module)
return module
从下面的代码能够看出根本的操作流程,屡次判断了模块是否存在在sys.modules
当中,所以说这个是第一搜寻指标,如果是有父类的话,path属性为父类的__path__
属性,也就是模块的门路,没有的话为None(后续应该会有默认值),最初在模块搜寻方面的次要办法是_find_spec
,在模块加载方面的次要办法是_load_unlocked
,到目前为止,咱们的筹备阶段曾经实现,上面正式进入搜寻、加载这两个阶段了。
2. Module是如何被发现的? — 深刻查找器Finder
对于查找器的线索咱们要从_find_spec
这个函数动手
# importlib/_bootstrap.py
def _find_spec(name, path, target=None):
"""Find a module's spec."""
# 获取meta_path
meta_path = sys.meta_path
if meta_path is None:
# PyImport_Cleanup() is running or has been called.
raise ImportError("sys.meta_path is None, Python is likely "
"shutting down")
if not meta_path:
_warnings.warn('sys.meta_path is empty', ImportWarning)
# We check sys.modules here for the reload case. While a passed-in
# target will usually indicate a reload there is no guarantee, whereas
# sys.modules provides one.
# 如果模块存在在sys.modules中,则断定为须要重载
is_reload = name in sys.modules
# 遍历meta_path中的各个finder
for finder in meta_path:
with _ImportLockContext():
try:
# 调用find_spec函数
find_spec = finder.find_spec
except AttributeError:
# 如果沒有find_spec屬性,则调用_find_spec_legacy
spec = _find_spec_legacy(finder, name, path)
if spec is None:
continue
else:
# 利用find_spec函数找到spec
spec = find_spec(name, path, target)
if spec is not None:
# The parent import may have already imported this module.
if not is_reload and name in sys.modules:
module = sys.modules[name]
try:
__spec__ = module.__spec__
except AttributeError:
# We use the found spec since that is the one that
# we would have used if the parent module hadn't
# beaten us to the punch.
return spec
else:
if __spec__ is None:
return spec
else:
return __spec__
else:
return spec
else:
return None
代码中提到了一个概念–sys.meta_path
,零碎的元门路,具体打印出看下它是什么
>>> import sys
>>> sys.meta_path
[
<class '_frozen_importlib.BuiltinImporter'>,
<class '_frozen_importlib.FrozenImporter'>,
<class '_frozen_importlib_external.PathFinder'>
]
后果能够看出,它是一个查找器Finder的列表,而_find_spec
的过程是针对每个Finder调用其find_spec
函数,那咱们就随便筛选个类看看他们的find_spec
函数是什么,就比方第一个类<class '_frozen_importlib.BuiltinImporter'>
2.1 规范的Finder
# importlib/_bootstrap.py
class BuiltinImporter:
"""Meta path import for built-in modules.
All methods are either class or static methods to avoid the need to
instantiate the class.
"""
@classmethod
def find_spec(cls, fullname, path=None, target=None):
if path is not None:
return None
# 判断是否是内置模块
if _imp.is_builtin(fullname):
# 调用spec_from_loader函数,由参数可知,loader为本身,也就是表明BuiltinImporter这个类不仅是个查找器也是一个加载器
return spec_from_loader(fullname, cls, origin='built-in')
else:
return None
def spec_from_loader(name, loader, *, origin=None, is_package=None):
"""Return a module spec based on various loader methods."""
# 依据不同loader的办法返回ModuleSpec对象
if hasattr(loader, 'get_filename'):
if _bootstrap_external is None:
raise NotImplementedError
spec_from_file_location = _bootstrap_external.spec_from_file_location
if is_package is None:
return spec_from_file_location(name, loader=loader)
search = [] if is_package else None
return spec_from_file_location(name, loader=loader,
submodule_search_locations=search)
if is_package is None:
if hasattr(loader, 'is_package'):
try:
is_package = loader.is_package(name)
except ImportError:
is_package = None # aka, undefined
else:
# the default
is_package = False
# 最初返回ModuleSpec对象
return ModuleSpec(name, loader, origin=origin, is_package=is_package)
<class '_frozen_importlib.BuiltinImporter'>
的find_spec
办法最终返回的是ModuleSpec
对象,而另一个类<class '_frozen_importlib.FrozenImporter'>
也是一样,只不过是他们对于模块的判断形式有所不同:_imp.is_builtin(fullname)
和_imp.is_frozen(fullname)
额定说一个知识点:对于is_frozen
的实现在Python/import.c
文件中
// Python/import.c
/*[clinic input]
// 转化成python的模式就是_imp.is_frozen函数
_imp.is_frozen
name: unicode
/
Returns True if the module name corresponds to a frozen module.
[clinic start generated code]*/
// 底层函数实现
static PyObject *
_imp_is_frozen_impl(PyObject *module, PyObject *name)
/*[clinic end generated code: output=01f408f5ec0f2577 input=7301dbca1897d66b]*/
{
const struct _frozen *p;
p = find_frozen(name);
return PyBool_FromLong((long) (p == NULL ? 0 : p->size));
}
/* Frozen modules */
static const struct _frozen *
find_frozen(PyObject *name)
{
const struct _frozen *p;
if (name == NULL)
return NULL;
// 循环比拟内置的frozen module
for (p = PyImport_FrozenModules; ; p++) {
if (p->name == NULL)
return NULL;
if (_PyUnicode_EqualToASCIIString(name, p->name))
break;
}
return p;,
}
那frozen module指的是什么呢?具体的内容能够在[Python Wiki[Freeze]](https://wiki.python.org/moin/…,简而言之,它创立了一个python脚本的可移植版本,它带有本人的内置解释器(基本上像一个二进制可执行文件),这样你就能够在没有python的机器上运行它。
从代码中还能够发现的变动是有个对于find_spec
办法的谬误捕捉,而后调用了_find_spec_legacy
办法,接着调用find_module
# _bootstrap.py
try:
# 调用find_spec函数
find_spec = finder.find_spec
except AttributeError:
# 如果沒有find_spec屬性,则调用_find_spec_legacy
spec = _find_spec_legacy(finder, name, path)
if spec is None:
continue
else:
# 利用find_spec函数找到spec
spec = find_spec(name, path, target)
def _find_spec_legacy(finder, name, path):
# This would be a good place for a DeprecationWarning if
# we ended up going that route.
loader = finder.find_module(name, path)
if loader is None:
return None
return spec_from_loader(name, loader)
那find_spec
和find_module
的关系是?
在PEP 451 — A ModuleSpec Type for the Import System中就曾经提供Python 3.4版本之后会以find_spec
来代替find_module
,当然,为了向后兼容,所以就呈现了咱们下面显示的谬误捕捉。
Finders are still responsible for identifying, and typically creating, the loader that should be used to load a module. That loader will now be stored in the module spec returned by find_spec() rather than returned directly. As is currently the case without the PEP, if a loader would be costly to create, that loader can be designed to defer the cost until later.
MetaPathFinder.find_spec(name, path=None, target=None)
PathEntryFinder.find_spec(name, target=None)
Finders must return ModuleSpec objects when find_spec() is called. This new method replaces find_module() and find_loader() (in the PathEntryFinder case). If a loader does not have find_spec(), find_module() and find_loader() are used instead, for backward-compatibility.
Adding yet another similar method to loaders is a case of practicality. find_module() could be changed to return specs instead of loaders. This is tempting because the import APIs have suffered enough, especially considering PathEntryFinder.find_loader() was just added in Python 3.3. However, the extra complexity and a less-than- explicit method name aren't worth it.
2.2 扩大的Finder
除了两个规范的Finder外,咱们还须要留神到的是第三个扩大的<class '_frozen_importlib_external.PathFinder'>
类
# importlib/_bootstrap_external.py
class PathFinder:
"""Meta path finder for sys.path and package __path__ attributes."""
# 为sys.path和包的__path__属性服务的元门路查找器
@classmethod
def _path_hooks(cls, path):
"""Search sys.path_hooks for a finder for 'path'."""
if sys.path_hooks is not None and not sys.path_hooks:
_warnings.warn('sys.path_hooks is empty', ImportWarning)
# 从sys.path_hooks列表中搜寻钩子函数调用门路
for hook in sys.path_hooks:
try:
return hook(path)
except ImportError:
continue
else:
return None
@classmethod
def _path_importer_cache(cls, path):
"""Get the finder for the path entry from sys.path_importer_cache.
If the path entry is not in the cache, find the appropriate finder
and cache it. If no finder is available, store None.
"""
if path == '':
try:
path = _os.getcwd()
except FileNotFoundError:
# Don't cache the failure as the cwd can easily change to
# a valid directory later on.
return None
# 映射到PathFinder的find_spec办法正文,从以下这两个门路中搜寻别离是sys.path_hooks和sys.path_importer_cache
# 又呈现了sys.path_hooks的新概念
# sys.path_importer_cache是一个finder的缓存,重点看下_path_hooks办法
try:
finder = sys.path_importer_cache[path]
except KeyError:
finder = cls._path_hooks(path)
sys.path_importer_cache[path] = finder
return finder
@classmethod
def _get_spec(cls, fullname, path, target=None):
"""Find the loader or namespace_path for this module/package name."""
# If this ends up being a namespace package, namespace_path is
# the list of paths that will become its __path__
namespace_path = []
# 对path列表中的每个path查找,要不是sys.path要不就是包的__path__
for entry in path:
if not isinstance(entry, (str, bytes)):
continue
# 再次须要获取finder
finder = cls._path_importer_cache(entry)
if finder is not None:
# 找到finder之后就和之前的流程一样
if hasattr(finder, 'find_spec'):
# 如果查找器具备find_spec的办法,则和之前说的默认的finder一样,调用其find_spec的办法
spec = finder.find_spec(fullname, target)
else:
spec = cls._legacy_get_spec(fullname, finder)
if spec is None:
continue
if spec.loader is not None:
return spec
portions = spec.submodule_search_locations
if portions is None:
raise ImportError('spec missing loader')
# This is possibly part of a namespace package.
# Remember these path entries (if any) for when we
# create a namespace package, and continue iterating
# on path.
namespace_path.extend(portions)
else:
# 没有找到则返回带有namespace门路的spec对象,也就是创立一个空间命名包的ModuleSpec对象
spec = _bootstrap.ModuleSpec(fullname, None)
spec.submodule_search_locations = namespace_path
return spec
@classmethod
def find_spec(cls, fullname, path=None, target=None):
"""Try to find a spec for 'fullname' on sys.path or 'path'.
搜寻是基于sys.path_hooks和sys.path_importer_cache的
The search is based on sys.path_hooks and sys.path_importer_cache.
"""
# 如果没有path就默认应用sys.path
if path is None:
path = sys.path
# 调用外部公有函数_get_spec获取spec
spec = cls._get_spec(fullname, path, target)
if spec is None:
return None
elif spec.loader is None:
# 如果没有loader,则利用命令空间包的查找形式
namespace_path = spec.submodule_search_locations
if namespace_path:
# We found at least one namespace path. Return a spec which
# can create the namespace package.
spec.origin = None
spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec)
return spec
else:
return None
else:
return spec
咱们先理解下代码中新呈现的一个概念,sys.path_hooks
,它的具体内容是
>>> sys.path_hooks
[
<class 'zipimport.zipimporter'>,
<function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x000001A014AB4708>
]
依据代码以及咱们标好的正文,能够大略缕出这样的逻辑(假如咱们应用的path是sys.path
,并且是第一次加载,没有波及到缓存)
对于两个钩子函数来说,<class 'zipimport.zipimporter'>
返回到应该带有加载zip文件的loader的ModuleSpec对象,那<function FileFinder.path_hook.<locals>.path_hook_for_FileFinder>
的逻辑咱们再去代码中看看
# importlib/_bootstrap_external.py
class FileFinder:
"""File-based finder.
Interactions with the file system are cached for performance, being
refreshed when the directory the finder is handling has been modified.
"""
def _get_spec(self, loader_class, fullname, path, smsl, target):
loader = loader_class(fullname, path)
return spec_from_file_location(fullname, path, loader=loader,
submodule_search_locations=smsl)
def find_spec(self, fullname, target=None):
"""Try to find a spec for the specified module.
Returns the matching spec, or None if not found.
"""
# Check for a file w/ a proper suffix exists.
for suffix, loader_class in self._loaders:
full_path = _path_join(self.path, tail_module + suffix)
_bootstrap._verbose_message('trying {}', full_path, verbosity=2)
if cache_module + suffix in cache:
if _path_isfile(full_path):
return self._get_spec(loader_class, fullname, full_path,
None, target)
if is_namespace:
_bootstrap._verbose_message('possible namespace for {}', base_path)
spec = _bootstrap.ModuleSpec(fullname, None)
spec.submodule_search_locations = [base_path]
return spec
return None
def spec_from_file_location(name, location=None, *, loader=None,
submodule_search_locations=_POPULATE):
"""Return a module spec based on a file location.
To indicate that the module is a package, set
submodule_search_locations to a list of directory paths. An
empty list is sufficient, though its not otherwise useful to the
import system.
The loader must take a spec as its only __init__() arg.
"""
spec = _bootstrap.ModuleSpec(name, loader, origin=location)
spec._set_fileattr = True
# Pick a loader if one wasn't provided.
if loader is None:
for loader_class, suffixes in _get_supported_file_loaders():
if location.endswith(tuple(suffixes)):
loader = loader_class(name, location)
spec.loader = loader
break
else:
return None
# Set submodule_search_paths appropriately.
if submodule_search_locations is _POPULATE:
# Check the loader.
if hasattr(loader, 'is_package'):
try:
is_package = loader.is_package(name)
except ImportError:
pass
else:
if is_package:
spec.submodule_search_locations = []
else:
spec.submodule_search_locations = submodule_search_locations
if spec.submodule_search_locations == []:
if location:
dirname = _path_split(location)[0]
spec.submodule_search_locations.append(dirname)
return spec
咱们能够从_get_supported_file_loaders
这个函数中里理解到可能返回的loader类型为
# importlib/_bootstrap_external.py
def _get_supported_file_loaders():
"""Returns a list of file-based module loaders.
Each item is a tuple (loader, suffixes).
"""
extensions = ExtensionFileLoader, _imp.extension_suffixes()
source = SourceFileLoader, SOURCE_SUFFIXES
bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES
return [extensions, source, bytecode]
也就是ExtensionFileLoader
,SourceFileLoader
,SourcelessFileLoader
,到目前为止咱们能够得出对于查找器Finder的大抵逻辑
3. Module是如何被加载的? — 深刻加载器Loader
对于查找器的线索咱们要从_load_unlocked
这个函数动手
# importlib/_bootstrap.py
def module_from_spec(spec):
"""Create a module based on the provided spec."""
# 创立module对象,把spec的所有属性都赋予给module,参考_init_module_attrs办法
# Typically loaders will not implement create_module().
module = None
if hasattr(spec.loader, 'create_module'):
# If create_module() returns `None` then it means default
# module creation should be used.
module = spec.loader.create_module(spec)
elif hasattr(spec.loader, 'exec_module'):
raise ImportError('loaders that define exec_module() '
'must also define create_module()')
if module is None:
module = _new_module(spec.name)
_init_module_attrs(spec, module)
return module
def _load_backward_compatible(spec):
# (issue19713) Once BuiltinImporter and ExtensionFileLoader
# have exec_module() implemented, we can add a deprecation
# warning here.
try:
spec.loader.load_module(spec.name)
except:
if spec.name in sys.modules:
module = sys.modules.pop(spec.name)
sys.modules[spec.name] = module
raise
def _load_unlocked(spec):
# 此时,咱们曾经拿到了具体的ModuleSpec对象,要由_load_unlocked帮咱们加载到零碎当中
# A helper for direct use by the import system.
if spec.loader is not None:
# Not a namespace package.
# 判断ModuleSpec对象是否具备exec_module办法
if not hasattr(spec.loader, 'exec_module'):
# 没有的话,则调用load_module的办法
return _load_backward_compatible(spec)
# 创立module对象
module = module_from_spec(spec)
# This must be done before putting the module in sys.modules
# (otherwise an optimization shortcut in import.c becomes
# wrong).
spec._initializing = True
try:
# 先占位,此时还未加载好module
sys.modules[spec.name] = module
try:
if spec.loader is None:
if spec.submodule_search_locations is None:
raise ImportError('missing loader', name=spec.name)
# A namespace package so do nothing.
else:
# 调用各个loader特有的exec_module,真正开始加载module相似于不同finder的find_spec办法
spec.loader.exec_module(module)
except:
try:
# 失败
del sys.modules[spec.name]
except KeyError:
pass
raise
# Move the module to the end of sys.modules.
# We don't ensure that the import-related module attributes get
# set in the sys.modules replacement case. Such modules are on
# their own.
module = sys.modules.pop(spec.name)
sys.modules[spec.name] = module
_verbose_message('import {!r} # {!r}', spec.name, spec.loader)
finally:
spec._initializing = False
return module
能够看出,在模块加载的时候绝对于查找逻辑更加清晰,和Finder同样的情理,load_module
和exec_module
的区别也是在Python3.4之后官网倡议的加载形式,对于具体的exec_module
的实现咱们大略说下
ExtensionFileLoader
: 调用builtin对象_imp.create_dynamic()
,在_PyImportLoadDynamicModuleWithSpec()
咱们看到了最终程序调用dlopen/LoadLibrary
来加载动态链接库并且执行其中的PyInit_modulename
。SourcelessFileLoader
:读取*.pyc
文件,而后截取16字节之后的内容,调用marshal.loads()
将读取的内容转换成code object
,而后调用builtin函数exec在module对象的__dict__
里执行这个code object
。SourceFileLoader
:逻辑相似,不过它会先调用编译器将代码转换成code object
。
最初,执行完code object
的module对象就算是加载实现了,它将被cache在sys.modules
里,当下次有调用的时候能够间接在sys.modules
中加载了,也就是咱们之前看过的一次又一次的判断module对象是否存在在sys.modules
当中。
三、import tricks
在理解了import
的外围流程之后,对于它的应用以及网上介绍的一些很tricks的办法,置信大家都能很快的理解它们的原理
1. 动静批改模块搜寻门路
对于import的搜寻阶段的批改,搜寻阶段是利用三种finder在path列表中去查找包门路,咱们想要动静批改搜寻门路的话能够采纳这两种形式:
- 扭转搜寻门路:向默认的
sys.path
中增加目录,扩充查找范畴 - 扭转
sys.meta_path
,自定义finder,定义咱们本人想要的查找行为,相似的应用比方从近程加载模块
2. import hook
同样也是利用扭转sys.meta_path
的办法或是更改sys.path_hooks
的办法,扭转import的默认加载形式。
3. 插件零碎
应用python实现一个简略的插件零碎,最外围的就是插件的动静导入和更新,对于动静导入的形式能够应用__import__
内置函数或者importlib.import_module
达到依据名称来导入模块,另外一个方面是插件的更新,咱们下面曾经理解到,当module对象被exec_module
的办法加载时,会执行一遍code object
并保留在sys.modules
当中,如果咱们想要更新某个module的时候,不能间接删除sys.modules
的module key再把它加载进来(因为可能咱们在其余中央会保留对这个module的援用,咱们这操作还导致两次模块对象不统一),而这时候咱们须要应用importlib.reload()
办法,能够重用同一个模块对象,并简略地通过从新运行模块的代码,也就是下面咱们提到的code object
来从新初始化模块内容。
发表回复