import是在一个模块中访问另一个模块的方法,那么第一个问题就是——什么是模块?

模块(Module)

什么是模块?
模块是一个包含python的声明和定义的文件,文件名为模块名加上.py后缀,每个模块拥有自己的命名空间。

包(Package)

什么是包?为什么设计包这个概念?包与模块有什么区别?
通常来说,包是一个包含多个模块的模块,例如一个包含多个.py后缀的目录。而设计这个包的概念是为了可以层级的组织模块。包是一种特殊情况下的模块,单不是所有模块都是包,最直接的区别就是包拥有__path__属性,而模块是没有的。
同时,Python定义了两种类型的包——常规包和命名空间包。

常规包(Regular Package)

通常我们印象中的包的结构都是常规包,如下所示

1
2
3
4
5
6
7
8
parent/
__init__.py
one/
__init__.py
two/
__init__.py
three/
__init__.py

目录中包含__init__.py文件,当我们引用该包或该包中的子模块时,对应的__init__会在被引用时隐式执行掉。

命名空间包(Namespace Package)

与常规包不同的是,命名空间包不需要最高层级的__init__.py,也就是上述中的parent/__init__.py,同时也不要求子包必须同时位于一个目录下,通常情况选择命名空间包时候,子包都是不位于同一目录的,例如一些包的扩展。

1
2
3
4
5
6
7
8
.
├── pkg_1
│ └── cum
│ └── lib_1.py
├── pkg_2
│ └── cum
│ └── lib_2.py
└── import_test.py
1
2
3
4
5
6
7
8
9
10
import sys

sys.path.extend(["./pkg_1", "./pkg_2"])

import cum
import cum.lib_1
import cum.lib_2

print(cum) # <module 'cum' (namespace)>
print(cum.__path__) # _NamespacePath(['/media/E/lichunyu/tmp/./pkg_1/cum', '/media/E/lichunyu/tmp/./pkg_2/cum'])

库(Library)

库就是包,只是Python官方将一些标准包称之为标准库

import

import做了什么

import做的事简单概括就是“创建模块并加入对应命名空间下”,那么创建模块是如何创建的呢?总的来说,模块总是从sys.modules获得。那么sys.modules是什么呢?sys.modules中总是有吗,如果没有会怎么样,没有的情况下为什么说总是从sys.modules获得呢?

sys.modules

简单来说这是一个缓存,保存着已经创建好的模块对象。那当缓存中不存在的话,就先创建该模块,然后加入缓存中,再返回缓存中的对应模块,所以最终总是从sys.modules获得模块对象。为什么要这样做呢?这样做的好处是什么?——因为模块可能会引用自身,如果不优先加入缓存并优先从缓存中获取,就会无限递归下去。

如何创建模块

那么如何创建模块呢?目前分为两步FindingLoading,查找是为了找到对应的模块,负责查找功能的对象称之为Finder,准确来说Finder返回的结果中,包含一个可调用函数,该函数称之为LoaderFinder遵循对应的协议,后面我们再简述对应的协议内容,之后通过Loader来创建模块对象。目前Python将整个引用过程完全暴露出来,方便大家学习同时提供了扩展能力,我们可以通过sys.meta_path获取默认的Finder,以Python 3.11为例

1
2
3
4
5
6
7
[
<_distutils_hack.DistutilsMetaFinder object at 0x7f1a6bd553d0>,
<class '_frozen_importlib.BuiltinImporter'>,
<class '_frozen_importlib.FrozenImporter'>,
<class '_frozen_importlib_external.PathFinder'>,
<pkg_resources.extern.VendorImporter object at 0x7f1a6ae303d0>
]

DistutilsMetaFinder

这个模块负责加载distutils, pip, sensitive_tests,主要是一些cpython相关的,我们不需要过于关注。

BuiltinImporter

顾名思义,引用的是内置的一些模块,所以我们自己的模块不要起和内置模块相同的名字,不然会导致默认情况下,没办法引用到我们的模块。

FrozenImporter

是一些frozen模块的Finder,类似一些把模块打包到一个可执行文件时,查找引用模块的组件。

VendorImporter

是打包一些扩展时会用的一个Finder,通过它可以对已有的某个命名空间进行扩展。

PathFinder

我们最后再来介绍这个Finder,因为它是我们日常中,会用到最多的一个。它通过按顺序查找sys.path列表,外层遍历sys.path内层遍历sys.path_hooks,直到找到可以处理对应path,为了防止每次import都重复这些,找到不同path是由哪个path hook来负责的都会缓存到sys.path_importer_cache,而sys.path是可以随意调整的,因此我们可以通过修改sys.path来更改不同位置模块被引用的优先级,但官方不建议直接修改sys.path,而是通过.pth文件或PYTHONPATH环境变量来扩展。

Loading

Loading过程大概如下

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
module = None
if spec.loader is not None and hasattr(spec.loader, 'create_module'):
# It is assumed 'exec_module' will also be defined on the loader.
module = spec.loader.create_module(spec)
if module is None:
module = ModuleType(spec.name)
# The import-related module attributes get set here:
_init_module_attrs(spec, module)

if spec.loader is None:
# unsupported
raise ImportError
if spec.origin is None and spec.submodule_search_locations is not None:
# namespace package
sys.modules[spec.name] = module
elif not hasattr(spec.loader, 'exec_module'):
module = spec.loader.load_module(spec.name)
# Set __loader__ and __package__ if missing.
else:
sys.modules[spec.name] = module
try:
spec.loader.exec_module(module)
except BaseException:
try:
del sys.modules[spec.name]
except KeyError:
pass
raise
return sys.modules[spec.name]

from xx import xx 和 import xx.xx as xx 的区别

对于日常使用第三方包来讲,非特别情况是完全一样的。但是from importimport后面既可以是模块也可以是其他对象,import asimport后面只能是模块,不过也不总是这样,你可以针对.的魔法方法进行重载来完成你需要的操作,最后有例子来展示这一点。

Hooks

import提供了两种hook——meta hooks, import path hooks

meta hooks

meta hooks会在import最开始被执行(在查找sys.modules之后),这个hook允许修改sys.path以及builtin modulesfrozen modules,只需要将其加入sys.meta_path既可

import path hooks

import path hooks是一个在处理sys.path时被调用的hook,可以向sys.path_hooks中加入。默认的sys.path_hooks存在以下两个Importer

1
2
<class 'zipimport.zipimporter'>
<function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x7f55b7d982c0>

当你了解了import的机制之后,可以做些什么?

远程加载模块

这是一个非常常见的例子,通过path hooks来检测模块是否位于远程端,通过网络传输过来的代码直接创建对应模块,此方法需要在sys.path加入远端服务的uri。同样的也可以采用meta hooks去直接检查指定uri远程模块,获取并创建。

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
class UrlPathFinder(importlib.abc.PathEntryFinder):
def __init__(self, baseurl):
self._links = None
self._loader = UrlModuleLoader(baseurl)
self._baseurl = baseurl

def find_loader(self, fullname):
log.debug('find_loader: %r', fullname)
parts = fullname.split('.')
basename = parts[-1]
# Check link cache
if self._links is None:
self._links = [] # See discussion
self._links = _get_links(self._baseurl)

# Check if it's a package
if basename in self._links:
log.debug('find_loader: trying package %r', fullname)
fullurl = self._baseurl + '/' + basename
# Attempt to load the package (which accesses __init__.py)
loader = UrlPackageLoader(fullurl)
try:
loader.load_module(fullname)
log.debug('find_loader: package %r loaded', fullname)
except ImportError as e:
log.debug('find_loader: %r is a namespace package', fullname)
loader = None
return (loader, [fullurl])

# A normal module
filename = basename + '.py'
if filename in self._links:
log.debug('find_loader: module %r found', fullname)
return (self._loader, [])
else:
log.debug('find_loader: module %r not found', fullname)
return (None, [])

def invalidate_caches(self):
log.debug('invalidating link cache')
self._links = None

# Check path to see if it looks like a URL
_url_path_cache = {}
def handle_url(path):
if path.startswith(('http://', 'https://')):
log.debug('Handle path? %s. [Yes]', path)
if path in _url_path_cache:
finder = _url_path_cache[path]
else:
finder = UrlPathFinder(path)
_url_path_cache[path] = finder
return finder
else:
log.debug('Handle path? %s. [No]', path)

def install_path_hook():
sys.path_hooks.append(handle_url)
sys.path_importer_cache.clear()
log.debug('Installing handle_url')

def remove_path_hook():
sys.path_hooks.remove(handle_url)
sys.path_importer_cache.clear()
log.debug('Removing handle_url')

懒加载(运行时加载)

msf,在__init__.py中,主动构造一个模块鸭子类型并至于sys.modules中。

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
# __init__.py
from typing import TYPE_CHECKING

from .utils import _LazyModule


__version__ = "0.2.0"


_import_structure = {
"engine": [
"Engine",
"get_app"
],
"core": [
"Graph",
"node_register"
],
"utils.import_utils": [
"load_register_node"
],
"utils.logging_utils": [
"logger_setup"
]
}


if TYPE_CHECKING:
from .engine import Engine, get_app
from .core import Graph, node_register
from .utils.import_utils import load_register_node
from .utils.logging_utils import logger_setup
else:
import sys

sys.modules[__name__] = _LazyModule(
__name__,
globals()["__file__"],
_import_structure,
module_spec=__spec__,
extra_objects={"__version__": __version__},
)

_LazyModule模块代码

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
import sys
import importlib
import os
from itertools import chain
import re

from . import logger


ModuleType = type(sys)


class _LazyModule(ModuleType):

def __init__(self, name, module_file, import_structure, module_spec=None, extra_objects=None):
super().__init__(name)
self._modules = set(import_structure.keys())
self._class_to_module = {}
for key, values in import_structure.items():
for value in values:
self._class_to_module[value] = key
self.__all__ = list(import_structure.keys()) + list(chain(*import_structure.values()))
self.__file__ = module_file
self.__spec__ = module_spec
self.__path__ = [os.path.dirname(module_file)]
self._objects = {} if extra_objects is None else extra_objects
self._name = name
self._import_structure = import_structure

def __dir__(self):
result = super().__dir__()
for attr in self.__all__:
if attr not in result:
result.append(attr)
return result

def __getattr__(self, name: str):
if name in self._objects:
return self._objects[name]
if name in self._modules:
value = self._get_module(name)
elif name in self._class_to_module.keys():
module = self._get_module(self._class_to_module[name])
value = getattr(module, name)
else:
raise AttributeError(f"module {self.__name__} has no attribute {name}")

setattr(self, name, value)
return value

def _get_module(self, module_name: str):
try:
return importlib.import_module("." + module_name, self.__name__)
except Exception as e:
raise RuntimeError(
f"Failed to import {self.__name__}.{module_name} because of the following error (look up to see its traceback):\n{e}"
) from e

def __reduce__(self):
return (self.__class__, (self._name, self.__file__, self._import_structure))

其他

还有很多其他的例子,例如扩展某个第三方包或者pytest一样像重载了assert一样等等,这里就不一一举例了。