司开星的博客

Python import 原理

本文主要讨论 Python 3 的 import。

导入步骤

import 语句主要执行以下两个步骤:

  1. 搜索模块;

  2. 搜索结果绑定到局部命名空间。

本文主要关注搜索步骤的逻辑。

搜索模块

搜索模块分为两个过程:

  1. 搜索 sys.modules

  2. 搜索 sys.meta_path

sys.modules

首次导入一个模块时相关程序会将此模块以及模块中调用到的其他模块信息以字典的形式保存到sys.modules 中,如果再次import此模块则会直接使用字典中的信息。

如果是通过 from package import moduleimport package.module 形式导入的模块则其父包(模块)也会被写入 sys.modules 中。

我们可以直接修改 sys.modules 来改变缓存信息:

1
2
3
4
import os
import sys
sys.modules['spp'] = os
print(spp)

sys.meta_path

如果在 sys.modules 中没有找到模块,则进入第二个搜索步骤:使用 sys.meta_path 搜索。

sys.meta_path 是保存了一系列 importer 对象的list。根据官方定义,importer 是指实现了 findersloaders 接口的对象。此处我们暂时不纠结于此定义,直接看 sys.meta_path 中有什么。

Python 3.6的 sys.meta_path中默认有以下三个 importer

  • class ‘_frozen_importlib.BuiltinImporter’
  • class ‘_frozen_importlib.FrozenImporter’
  • class ‘_frozen_importlib.PathFinder’

三个 importer 的用途分别是:查找及导入build-in模块;查找及导入frozen模块(即已编译为Unix可执行文件的模块);查找及导入import path中的模块。

如果这三个 importer 中都没找到模块则会抛出 ModuleNotFoundError。

import hooks

如果要扩展 import 的行为,需要用到import hooks。

Python 中有两种import hooks:meta hooks 和 path hooks。

meta hooks

meta hooks 简单来说是通过修改 sys.meta_path 达到拦截导入目的。

上面提到过,Importer 对象需要实现 finders 协议和 loaders 协议。其中finders协议需要提供 find_spec() 方法(Python3.4以后的推荐方法)或 find_modules()方法。此处我们自己定义两个 importer (暂不考虑loaders协议)来演示如何进行meta hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Importer1(object):

def find_module(self, fullname, path): # 同时定义find_module和find_spec看看会调用哪个方法
print('find_module Looking for', fullname, path)
return None

def find_spec(self, fullname, path, args):
print('find_spec Looking for', fullname, path)
return None # 返回None向编译器说明此处无法找到这个module


class Importer2(object):

def find_module(self, fullname, path):
print('find_module2 Looking for', fullname, path)
return None

def find_spec(self, fullname, path, args):
print('find_spec2 Looking for', fullname, path)
return None

把两个 importer 分别插入 sys.meta_path 的首尾:

1
2
3
import sys
sys.meta_path.insert(0, Importer1())
sys.meta_path.append(Importer2())

测试导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
>>>import json

find_spec Looking for json None
find_spec Looking for json.decoder ['C:\\Program Files (x86)\\Python36\\lib\\json']
find_spec Looking for json.scanner ['C:\\Program Files (x86)\\Python36\\lib\\json']
find_spec Looking for _json None
find_spec Looking for json.encoder ['C:\\Program Files (x86)\\Python36\\lib\\json']

>>>json

<module 'json' from 'C:\\Program Files (x86)\\Python36\\lib\\json\\__init__.py'>

>>>import jso

find_spec Looking for jso None

find_spec2 Looking for jso None

Traceback (most recent call last):
File "<input>", line 1, in <module>
File "*\pydev_import_hook.py", line 21, in do_import
module = self._system_import(name, *args, **kwargs)
ModuleNotFoundError: No module named 'jso'


>>>jso

Traceback (most recent call last):
File "<input>", line 1, in <module>
NameError: name 'jso' is not defined

首先我们导入了标准库中的json, 发现只调用了Importer1中的find_spec方法,并且最后导入成功。之后导入一个不存在的模块,Importer1 中的 find_specImporter2 中的 find_spec 方法均被调用到,且模块没有导入成功。

通过上面的示例可以看出使用meta hooks的方式:定义符合协议的 importer 并插入 sys.meta_path 中。 sys.meta_path 是有序列表,如果想尽早拦截 import 则需将自定义的 importer 插入 sys.meta_path首位。

Python源码中的BuiltinImporter写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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.

"""

@staticmethod
def module_repr(module):
"""Return repr for the module.

The method is deprecated. The import machinery does the job itself.

"""
return '<module {!r} (built-in)>'.format(module.__name__)

@classmethod
def find_spec(cls, fullname, path=None, target=None):
if path is not None:
return None
if _imp.is_builtin(fullname):
return spec_from_loader(fullname, cls, origin='built-in')
else:
return None

@classmethod
def find_module(cls, fullname, path=None):
"""Find the built-in module.

If 'path' is ever specified then the search is considered a failure.

This method is deprecated. Use find_spec() instead.

"""
spec = cls.find_spec(fullname, path)
return spec.loader if spec is not None else None

@classmethod
def create_module(self, spec):
"""Create a built-in module"""
if spec.name not in sys.builtin_module_names:
raise ImportError('{!r} is not a built-in module'.format(spec.name),
name=spec.name)
return _call_with_frames_removed(_imp.create_builtin, spec)

@classmethod
def exec_module(self, module):
"""Exec a built-in module"""
_call_with_frames_removed(_imp.exec_builtin, module)

@classmethod
@_requires_builtin
def get_code(cls, fullname):
"""Return None as built-in modules do not have code objects."""
return None

@classmethod
@_requires_builtin
def get_source(cls, fullname):
"""Return None as built-in modules do not have source code."""
return None

@classmethod
@_requires_builtin
def is_package(cls, fullname):
"""Return False as built-in modules are never packages."""
return False

load_module = classmethod(_load_module_shim)

finders 与 loaders

上面的例子已经演示了 finders 协议。finders协议主要任务是:根据方法定义查找模块,如果找不到则返回None,如果找到则返回一个spec类型实例(Py3.4之前是返回 loaders)。 spec实例封装了一些导入模块所需信息,以及loader方法,loader方法需实现loaders协议功能。

loaders 主要任务是:在找到模块后做一些初始化操作。loaders 必须要实现exec_module() 方法(在py3.4之前需要实现load_module()方法)。另外还可以实现可选的 create_module() 方法,用以创建模块。

path hooks

另一种import hookspath hooks, 作用于sys.meta_path 中的 PathFinderPathFinder 也是一个 Importer, 意味着它实现了 find_spec 方法。但由于不同路径的模块文件类型不同,在不同的路径下搜索需要实现不同的逻辑,并且判断路径的处理逻辑也是耗时的操作。

Python中用以下方法处理此问题:PathFinder 仍然作为入口,但具体处理逻辑(也就是针对具体情况的Importer代码)独立为其他模块。

当有新的搜索需求到达 PathFinder 时它首先开始迭代搜索路径(默认为sys.path),并将路径依次传递给sys.path_hooks中的可调用对象,如果某个对象可以处理此路径则将其写入 sys.path_importer_cache中并返回此结果用于接下来的模块搜索。之后如果再次遇到此路径则会直接用 path_importer_cache 中缓存的信息处理。

下面是源码中 PathFinder 的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

class PathFinder:

"""Meta path finder for sys.path and package __path__ attributes."""

....................

@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)
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
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 = []
for entry in path:
if not isinstance(entry, (str, bytes)):
continue
finder = cls._path_importer_cache(entry)
if finder is not None:
if hasattr(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:
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'.

The search is based on sys.path_hooks and sys.path_importer_cache.
"""
if path is None:
path = sys.path
spec = cls._get_spec(fullname, path, target)
if spec is None:
return None
elif spec.loader is None:
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 = 'namespace'
spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec)
return spec
else:
return None
else:
return spec

我们直接定义一个用于判断的函数并将其加入path_hooks即可。下面是《Python cookbook》中用于判断路径是否是URL的示例代码:

1
2
3
4
5
def check_url(path):
if path.startswith('http://'):
return Finder()
else:
raise ImportError()

如果此路径符合要求则返回处理此路径的 Importer ,否则抛出 ImportError 错误,PathFinder 会继续尝试path_hooks中的下一个。

返回的 Importer 对象和前面的 Importer 类似,也需要定义 find_spec()。更多细节请参考官方文档。

参考资料: