Python 模块热插拔的一种简单实现

这篇文章记录一种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 机制,可以看看另外一篇博客 

Python 包、模块和 import 笔记-CSDN博客

你可能感兴趣的:(知识系列,Python,python,笔记,开发语言)