在 python 中,在导入 module 背后究竟发生了什么?

1,024 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

今天我们的选择越来越多,不过由于每个人精力的有限,仿佛我们选择又不是那么多,所以现在如何选择变得更为重要,今天在编程领域,可用语言非常多,如何选择一个适合自己领域的语言,以及如何选择一个更有价值语言,所谓价值对于自己来就是带来更高收益。

module 对象

python 这门语言看起来比较简单,容易上手,特别是对于已有其他语言编程基础的人,会觉得 python 这门语言并不复杂,其实不然。今天我们就来说一说 python 中 module,我们创建一个文件 module_one.py 代码如下

print("starting module one")

x = 21

def say_hi():
    print("hello world")

在 python 中, module 也是一个对象(object), 引入一个模块其实主要做了 2 件事,首先在内存中为模块开辟一段空间,模块名称做键值保存在当前 global namespace dictionary 也就是全局命名空间字典里, 值这是指向这个模块的内存地址。这样行为和使用赋值符号为变量进行赋值并没有什么区别。

screenshot_001.png

{'__builtins__': <module '__builtin__' (built-in)>, 'module_one': <module 'module_one' from 'module_one.pyc'>, 'x': 27, '__name__': '__main__', '__package__': None, '__doc__': None}

这里 module_one 是模块名称,对应键值为模块的地址

'module_one': <module 'module_one' from 'module_one.pyc'>,

可以 type 来查看一下模块类型,显示类型为 module 如下

>>> type(module_one)
<type 'module'>

在 python 中,module object 是 types.ModuleType 类的一个实例

>>> import types
>>> isinstance(module_one,types.ModuleType)
True

和其他 python 对象(object)一样,可以通过 id(module_one) 来获取存放 module_one 对象的地址。

hex(id(module_one))

既然我们知道了 module 是 types.ModuleType 类来创建一个 module,第一参数是 module 名称,二个参数 module 的类型

>>> mod = types.ModuleType('mod','this is a test module')
>>> type(mod)
<type 'module'>

也可以为模块起一个别名,这里对于 module_one 的别名都是指向同一块内存地址的变量

import module_one

mod_one = module_one

print(mod_one)
print(hex(id(module_one)))
print(hex(id(mod_one)))

module 属性

我们创建一个 module_two.py 文件,然后在文件中,创建一个全局变量 x 和一个方法 my_func ,然后在方法 my_func 中输出一个变量 __file__,不难发现我们并没有定义 __file__ 这个变量

x = 21
def my_func():
    print(__file__)

当我们在其他文件中引入 module_two 这个模块,然后我们用 python 提供 dir 查看 module_two 的属性。

>>> import module_two
>>> dir(module_two)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'my_func', 'x']
>>> 

通过查看 module_two 属性,我们发现当一个文件做 module 导入到另一个文件作为 module 对象使用,其全局定义变量和方法称为这个 module 对象的属性。

同时这里 python 还为这个 module 对象创建了例如 __name____file__ 这样属性。这些属性可以作为全局变量来访问,

我们可以通过 module_two.__dict__ 可以方法模块的一些属性值

{... '__file__': 'module_two.pyc', '__package__': None, 'my_func': <function my_func at 0x107fa1398>, 'x': 21, '__name__': 'module_two', '__doc__': None}
>>> module_two.__dict__['x']
21
>>> module_two.__dict__['__name__']
'module_two'
>>> 

python 还提供了更直接方法来访问这些属性就是module_two.x 来直接访问这些属性值。

>>> import module_two
>>> module_two.x
21
>>> module_two.__file__
'绝对路径/module_two.py'
>>> module_two.my_func()
绝对路径/module_two.py
>>> module_two.__dict__['my_func']
<function my_func at 0x10b7b04c0>
>>> module_two.__dict__['my_func']()
绝对路径/module_two.py

这里值得注意,导入 module 并不一定都是 python 编写的 module, 其中还包括 c 或者 c++ 编译好的文件

>>> import math
>>> math.__file__
'路径/lib/python3.8/lib-dynload/math.cpython-38-darwin.so'

Finder 和 Loader

在 python 中,sys 模块的 meta_path 提供了一些导入 module 的方法,接下里我们就来看一看。

>>> import sys
>>> sys.meta_path
[<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]

sys 的 meta_path 有一系列对象(object) 在 python 导入系统中,有finder 和 loader 两个对象,所以想要理解这些对象,我们首先需要清楚什么是 finder 和 loader,从名字不难看出,finder 是负责定位 module 而 loader 则是负责加载 module。

finder 是一个类,具有一个实例方法 find_spec(fullname,path,target=None) 而 loder 是一个具有实例方法load_module(fullname,)

在 meta_path 中所有对象都是实现了 find_spec 方法,所以他们都是 finder 的具体实现。有的对象既实现了 finder 又实现了 loader 例如 Importer

通过名字不看看出 BuiltinImporter 和 FrozenImporter 是 Importer 而 PathFinder 是 finder

>>> importer = sys.meta_path[0]
>>> importer
<class '_frozen_importlib.BuiltinImporter'>
>>> sys.builtin_module_names
('_abc', '_ast', '_codecs', '_collections', ... 'builtins', 'errno', 'faulthandler', 'gc', 'itertools', 'marshal', 'posix', 'pwd', 'sys', ... )
>>> spec = importer.find_spec('itertools')
>>> spec
ModuleSpec(name='itertools', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in')
>>> 

首先我们将 sys.meta_path[0] 用变量 importer 来引用,查看类型这是内置模块,内置模块不同于其他 python 库,内置模块是会编译到解释器中。我们可以通过 sys.builtin_module_names 来查看内置模块。

importer 是一个 finder 的实例,所以可以调用 find_spec 来定位要加载模块,我们输出 spec 这里给出使用了哪一个 finder _frozen_importlib.BuiltinImporter

>>> spec.loader.load_module('itertools')
<module 'itertools' (built-in)>

我们回头看一看 find_spec 返回对象 spec 是一个 ModuleSpec 实例,其中一个属性为 loader ,所以我们可以直接使用 loader 属性来对 itertools 进行加载

这里有点 confusing,也就是 spec.loader 和 importer 其实是一个

>>> spec.loader == importer
True
>>> importer.load_module('itertools')
<module 'itertools' (built-in)>

pathFinder

pathFinder 可以不同目录下来搜索 module ,都包括哪些路径,可以通过 sys.path 来查看其搜索范围。

>>> importer = sys.meta_path[-1]
>>> importer
<class '_frozen_importlib_external.PathFinder'>
>>> sys.path
['', '路径/py38/lib/python38.zip',
 '路径/py38/lib/python3.8', 
 '路径/py38/lib/python3.8/lib-dynload', 
 '路径/py38/lib/python3.8/site-packages']
>>> spec = importer.find_spec('socket')
>>> spec
ModuleSpec(name='socket', loader=<_frozen_importlib_external.SourceFileLoader object at 0x10b76ba00>, origin='/路径/lib/python3.8/socket.py')
>>> 

module 加载过程

当执行 import 语句来导入一个 module 时,首先 python 会在全局缓存中搜索是否已经加载过了该 module ,这个全局缓存是一个名字为 module 名,值为 module 对象的键值对的字典。如果 module 已经存在于缓存中,则立即返回。 如果在 cache 中找不到 python 就会通过 finder 和 loader 来定位加载该 module,loader 在内存中创建一个 module 对象

>>> sys.modules

通过 sys.module 可以来查看已经加载的模块,

>>> del module_two

就是在全局命名空间将 module_two 移除指向 module_two 对象内存地址的变量而已,当我们再次调用 import module_two 不会重新对 module_two 对象进行编译。

我们可以在 sys.modules 添加一个模块,然后将模块赋值给函数,随后导入这个模块,输出其 type 发现这个不再是 module 类而是一个 function 然后就可以直接调用