乐趣区

关于python:面试官一个小时逼疯面试者之聊聊Python-Import-System

对于每一位 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.XPython 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

劣势:

  1. 代码档次清晰:能够很清晰的理解每条导入数据的全门路,不便咱们及时找到具体引入地位。
  2. 打消绝对地位依赖的问题:能够很不便的执行独自的 py 文件,而不必思考援用出错、绝对地位依赖等等问题。

劣势:

  1. 顶层包名硬编码:这种形式对于重构代码来说将会变得很简单,你须要查看所有文件来修复硬编码的门路(当然,应用 IDE 能够疾速更改),另一方面如果你想挪动代码或是别人须要应用你的代码也是很麻烦的事件(因为波及从根目录开始写门路,当作为另一个我的项目的子模块时,又须要扭转整个我的项目的包名构造)PS:当然能够通过打包解决。
  2. 导入包名过长:当我的项目层级过于宏大时,从根目录导入包会变得很简单,不仅须要理解整个我的项目的逻辑、包构造,而且在包援用多的时候,频繁的写门路会变得让人很焦躁。

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

劣势:

  1. 援用简洁:和相对导入相比,援用时能够依据包的绝对地位引入,不必理解整体的我的项目构造,更不必写简短的绝对路径,比方咱们能够把 from a.b.c.d.e.f.e import e1 变成from . import e1

劣势:

  1. 应用困扰:相比于绝对路径,相对路径因为须要更加明确绝对地位,因而在应用过程中常会呈现各种各样的问题,比方上面这些案例

假如咱们把我的项目构造改成这样

└── 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:

    1. Standard library imports.
    2. Related third party imports.
    3. 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_NAMESTORE_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__ 函数的调用,传入了方才咱们没有解释的参数 fromlistlevel,咱们能够通过查看 __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_specfind_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]

也就是ExtensionFileLoaderSourceFileLoaderSourcelessFileLoader,到目前为止咱们能够得出对于查找器 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_moduleexec_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 来从新初始化模块内容。

退出移动版