这篇文章记录一种Python 包内模块热插拔的简单实现,可以自行扩展和优化性能,这里主要提供一种思路,所以主要内容是实现功能
步骤拆分
完成这件事情分几步,创建目录结构,然后依据需求和问题逐步补充,这也是简单事情的处理逻辑
目录结构
先捋一下目录结构,demo.py 是我们用来测试的脚本,package 是可以热插拔的包,里面是模块
hotPlug
├── demo.py
└── package
├── __init__.py
├── moudle1.py
├── moudle2.py
└── moudle3.py
注意:Python 包必须要有 __init__.py 文件,否则不会被解释器认为是包
模块内容
为了保持内容一致,打印内容有不同,每个moudle文件内都是如下内容
# 用来调用
def main():
import datetime
print(datetime.datetime.now())
print(__file__)
# 加载到时会被执行
print(__file__)
# python 直接执行文件才会为 True
if __name__ == '__main__':
main()
调试 demo
demo 里面主要是演示热插拔,所以需要逐步完成
一)导入package
尝试导入 package 和模块看看会发生什么
# file: demo.py
# 导入包是不会有内容输出的
import package
# 会打印文件路径
import pacakge.moudle1
# 发现仍然没有输出
from package import *
# 如下内容会报错,说没有这个模块
from pacakge import moudle1
结果:
- 导入包不会执行模块的代码
- 导入包下的模块时会执行模块代码
- 奇怪,from 的方式为啥找不到模块呢?因为from 的导入需要查询包的__all__数组
Python 包、模块和 import 笔记-CSDN博客 可以看看这篇博客对 import 机制的介绍
二)编辑 __init__.py 文件
因为 import 机制的原因,我们需要在 __init__.py 维护 __all__ 来保持模块信息
# file: __init__.py
__all__ = ['moudle1', 'moudle2', 'moudle3']
此时,再尝试from package import moudle 就可以正常导入了
但是不能加一个就改一次吧,那么就需要自动添加了,遍历 package ,将 module 加入清单
# file: __init__.py
import os
# 获取当前包的子模块
def get_submodules(package_dir):
submodules = []
for item in os.listdir(package_dir):
if item.endswith('.py') and item != '__init__.py':
submodule_name = item[:-3]
submodules.append(submodule_name)
return submodules
package_dir = os.path.dirname(__file__)
submodules_list = get_submodules(package_dir)
# 子模块清单
__all__ = submodules_list
注意:
1)TypeError: Item in package.__all__ must be str, not module 这个告警提示我们,里面必须是str
2)在cmd 窗口实测,hotPlug工作目录,即使不添加 __all__ 数组,from 的导入也没有问题
三)验证热插拔
我们需要验证热插拔,那就需要一段测试脚本,在执行demo.py的过程中,我们复制module1.py 到 module 为module5.py ,以此来测试热插拔是否生效
# file: demo.py
for i in range(30):
from package import *
time.sleep(2)
结果:
- 由于module 代码有 print(__file__),可以认为执行了就导入了模块
- 发现只打印了一遍文件名,而且动态添加的module5.py 并未被打印
这是还是因为 Python 的 import 机制的问题,Python将模块导入后会加入到内存,会认为package已经导入到内存了,代码和结构变化都不会生效,必须等释放重新加载才行,而且导入是一种耗时的高成本操作,一般不会随意释放这些内存
四)增加重新加载包
因为包加入内存后,代码和包结构的变更不会生效,于是需要引入包的重新加载。最简单的当然是重启解释器,但是重启解释器就不叫热插拔了。
# file: demo.py
# 用来导入模块
import importlib
for i in range(30):
# 使用 reload 函数重新加载模块
importlib.reload(package)
from package import *
time.sleep(2)
结果:
- 这会儿,我们在执行过程中变更包下的模块数量和模块内容都会生效,热插拔成功了?
- 问题来了,我怎么知道热插拔变化的 module 是什么?怎么用?
还记得上面的 __all__ 数组不,这玩意得用起来
五)获取子模块
动态获取子模块,用来导入
# file: demo.py
# 用来导入模块
import importlib
for i in range(30):
# 使用 reload 函数重新加载模块
importlib.reload(package)
# 获取包的全部子模块
for module in package.__all__:
import module
time.sleep(2)
结果:
- 哦,报错了,说 No module named 'module',原来里面是字符串,不是对象
- 问题来了,字符串怎么导入呢?
六)模块名字符串导入
这时候又得上 importlib 了,最终版
import package
for i in range(30):
# 模块的集合
module_set = set()
# 使用 reload 函数重新加载模块
importlib.reload(package)
# 获取包的全部子模块
for module in package.__all__:
# import 包的子模块
module_set.add(importlib.import_module("package."+module))
time.sleep(2)
# 遍历子模块
for module in package_set:
# 调用子模块的函数
module.main()
结果:
- 模块的变化会随着 reload 生效,另外我们也可以通过模块的集合来获取子模块的列表
总结
至此,我们依据这几步完成了一个简单的 Python 包模块热插拔,因为这篇主要是实现思路,还存在很大的优化空间,比如 __all__ 的维护并没有遍历可能存在的子包,比如可以通过并发编程来提升子模块的导入效率,这篇用来抛砖引玉。
几点建议
1、包和模块的导入会比较耗时和消耗内存,所以尽量要避免重复导入
2、模块的导入会执行模块代码的,所以尽量用来复用的模块尽量不要有耗时的代码,如有需要可以包装成函数或者类来延迟耗时操作的执行
3、这个只是一个简单示例,可能性能上会很差,使用需要考虑性能问题
4、关于包和模块以及 import 机制,可以看看另外一篇博客