Python动态导入协议模块后无法访问新增属性如何解决
问题背景
- 开发了一款支持多通信协议的程序,设计逻辑为:程序启动时由用户选择目标通信协议,再动态导入对应协议的支撑模块。
第一次实现及问题
最初在protocol.py中实现provide_protocol函数,逻辑是将用户所选协议模块的所有非双下划线包裹的特殊成员注入到protocol模块命名空间,代码如下:
def provide_protocol( name ): for key, val in vars( __import__( name ) ).items(): if key.startswith( "__" ) and key.endswith( "__" ): continue vars()[key] = val importlib.invalidate_caches()
参数name为程序启动时用户传入的协议模块名。
运行故障:protocol.py会在provide_protocol函数执行前被导入,程序其他位置无法访问后续注入到该模块的新增函数,即便调用importlib.invalidate_caches()刷新模块缓存也无法解决。
第二次实现及问题
后续尝试移除protocol.py,完全基于importlib动态创建protocol模块,在对应包的__init__.py中编写代码如下:
protocol = None def provide_protocol( name ): protocol = importlib.import_module( name ) importlib.invalidate_caches()
运行故障:问题和第一次完全一致,其他位置导入的protocol变量始终为None,无法获取动态导入的目标协议模块。
故障原因
两个实现踩的都是Python作用域的基础坑,跟模块缓存半毛钱关系没有,这里的importlib.invalidate_caches()完全用错了地方——它是用来清理import系统查找模块时的内部缓存的,根本不会修改已经导入的模块内容,也不会更新已经存下来的变量引用。
- 第一个实现的问题:函数内部调用的
vars()如果不传参数,返回的是当前函数的局部命名空间字典,循环赋值的所有内容都只存在于provide_protocol函数的局部作用域里,函数执行完就销毁了,根本没写到protocol模块的全局命名空间里,外部当然访问不到。 - 第二个实现的问题:函数内部直接给
protocol变量赋值时,Python会默认这是函数内部的局部变量,赋值操作只改了这个临时的局部变量,外层作用域里初始值为None的protocol根本没被修改,其他位置导入的永远是最开始那个None值。
修正方案
针对第一种注入模块成员的写法
要修改模块的全局命名空间,需要拿到模块对象本身再操作,不要在函数里直接用无参的vars():
import importlib import sys def provide_protocol(name): # 拿到当前protocol模块自身的对象 current_module = sys.modules[__name__] # 导入目标协议模块 target_protocol = importlib.import_module(name) # 把目标模块的成员注入到当前模块的全局命名空间 for key, val in vars(target_protocol).items(): if key.startswith("__") and key.endswith("__"): continue setattr(current_module, key, val)
针对第二种直接暴露模块对象的写法
给函数内的变量加global声明,告诉Python你要修改的是外层的全局变量,而不是定义局部变量:
import importlib protocol = None def provide_protocol( name ): # 声明要修改全局作用域的protocol变量 global protocol protocol = importlib.import_module(name)
注意:使用该写法时,必须保证程序启动流程中先执行完
provide_protocol完成协议加载,再执行业务模块的导入逻辑,否则业务模块拿到的还是初始的None值。
内容的提问来源于stack exchange,提问作者leviathan

