前言
你是否也会为 reload Python 的模块干到烦恼。
需要在不同的脚本加上 reload 导入的模块确保可以看到代码的更新。
Python 是怎么缓存 import 的模块的。
TLDR;
我后来了解了 Python 的加载机制之后弄了一个函数,只要将我们开发的包命名加上,就可以实现整个开发包 reload 。
1 | def module_cleanup(module_name): |
如果我们的 test_module 下有众多脚本就不需要逐个去添加 reload 了。
万一不小心把 reload 发布出去了也会稍微降低脚本运行的性能。
Python Import
https://docs.python.org/3/reference/import.html
上面是 Python 的官方文档讲述 Python的 import 的时候背后的运行机理,也可以切换成中文进行阅读。
这里我将上面的文章结合自己的实践总结一番。
Python import 模块可以用关键字
import
或者importlib.import_module()
备注: 关键字调用无法放到 lambda 函数里面,这也是为什么 Python2 下默认
使用import
关键字其实背后执行的是__import__()
内置方法。
import 触发之后会从sys.modules
查找缓存,找不到就从sys.path
里面匹配模块 (这个过程也会触发 meta_path 等触发自定义的 import 行为)
找到匹配的模块就会创建模块 否则raise ModuleNotFoundError
生成的模块会放入到sys.modules
进行缓存。
import 执行操作(不考虑自定义 import 情况)
- 从
sys.modules
查找模块缓存 - 从
sys.path
匹配脚本 生成模块 放入sys.modules
缓存
sys.modules
由于
sys.modules
的缓存机制,Python 下次导入就从已经加载的缓存中获取模块,导致模块用的还是旧的代码逻辑。
相应的也可以修改 sys.modules 的字典实现骚操作
1 | import sys |
当然这种骚操作不推荐使用就是了。
另外还有一些危险的操作,比如del sys.modules["builtins"]
会让 Python 变得不正常(:з」∠)
1 | del sys.modules["builtins"] |
基于这个原理,如果将缓存清理了,下次 Python import 就会重新加载这个模块,实现 reload 的效果。
我最初也是在 mGear 的代码里面学习它们的 reload 方法学习到的。
它背后实现的代码就是
del sys.modules["mgear"]
等相关的模块
https://docs.python.org/3/reference/import.html#the-module-cache
根据官方文档的说明,如果一个大模块下有很多子模块,都是单独键值缓存的。
所以要 reload 所有的子模块需要编译键值将匹配的都删除掉。
1 | def module_cleanup(module_name): |
这个就是我整理的遍历所有匹配的模块进行缓存删除的函数,
sys.builtin_module_names
通过规避对内置模块的清理。
这样源代码不需要添加 reload ,我们只在开发用的调试脚本添加这个函数执行 reload 即可。
另外有一个小小注意点,用这个删除缓存的方式 reload 会将之前的 module 删除生成新的 module 对象,但是如果用reload
的话是沿用之前的 module 对象。
目前我实践上还没遇到过因为这个导致出现问题的情况。
packages 命名空间包
https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
按照上面链接提供的目录结构
1 | mynamespace-subpackage-a/ |
然后就可以
from mynamespace import subpackage_b
from mynamespace import subpackage_a
用同一个mynamespace
包导入两个不同路径的模块。
但是上面的链接也提到 命名空间包并不适用所有的情况,反而是用前缀包会更好。
模块遍历查找
1 | import pkgutil |
通过
pkgutil.walk_packages
可以遍历一个模块所有的子模块。
from setuptools import find_packages
也可以实现类似的功能
但是find_packages
面对命名空间模块不好使,但是walk_packages
好使。(原因是find_packages
通过os.walk
去查找路径的)
也可以通过这个方式将对应模块的缓存进行删除~
判断模块是否存在
1 | def importable(module_name) |
过去判断一模块是否可以 import 通常使用异常进行处理。
其实pkgutil.find_loader
也可以返回模块是否可以 import
1 | import pkgutil |
上面的方式就不需要用 exception 进行处理。(find_loader 的源码已经有 exception 的逻辑)
如果模块可以导入会返回对应的
loader
,使用load_module
可以进行加载。
注: py2 的load_module
必须要传参。
自定义 import 行为
除了
sys.path
通过系统路径查找 python 包进行加载之外。
Python 还有sys.meta_path
存储一系列 Finder 类 (Py3还需要Loader
类) 来自定义 import 逻辑。
1 | import sys |
上面的代码实现了 py2 py3 的 Finder 兼容。
可以实现加载任意名称的模块都能成功返回而不会引发 ImportError
当然这种操作如果用到项目里面,肯定会被人打死 😄
在 Py2 环境下 Finder 需要实现
find_module
和load_module
方法
Py3 环境可以参考下面的链接。
https://stackoverflow.com/a/58275573/13452951
需要有 Finder 需要实现
find_spec
返回ModuleSpec
类,这个类需要有Loader
进行加载逻辑
官方提供的
zipimport.zipimporter
在 Py2 下是 Finder ,在 Py3 下是 Loader。
可以从下面官方文档的类方法中看出来。
https://docs.python.org/2.7/library/zipimport.html?highlight=zip#module-zipimport
https://docs.python.org/3.10/library/zipimport.html?highlight=zip#module-zipimport
通过需改 import 机制,可以实现很多黑科技,但是推荐使用侵入性较小的使用方式。
这个机制可以让某个模块虚空导入而不报错,这不符合正常使用 Python 的逻辑,可能会让团队其他人很懵逼的。
如果某个 BUG 是因为这个机制导致的,其他人又不熟悉这块的话,那这问题查半天也不一定有结果 😢
这种黑科技的方式无法支持 mypy 类型检测和回溯,倒是可以做一些代码桩来实现提示,但不是很推荐。
总结
本次深入浅出地学习了 Python Import 的各种底层逻辑。
以后有机会的话也想好好学习一下 CPython 的底层实现。