多模块下使用__init_subclass__出现异常行为的原因探究
Python模块导入与PEP-487子类注册的坑
问题场景
测试PEP-487的子类注册机制时,将代码拆分到两个模块后出现了不符合预期的结果:
代码示例
a.py
class PluginBase: subclasses = [] def __init_subclass__(cls, **kwargs): print(f'__init_subclass__({cls!r}, **{kwargs!r})') super().__init_subclass__(**kwargs) cls.subclasses.append(cls) if __name__ == '__main__': from b import Plugin1, Plugin2 print('a:', PluginBase.subclasses)
b.py
from a import PluginBase class Plugin1(PluginBase): pass class Plugin2(PluginBase): pass print('b:', PluginBase.subclasses)
第一次执行结果
运行python a.py后输出:
__init_subclass__(<class 'b.Plugin1'>, **{}) __init_subclass__(<class 'b.Plugin2'>, **{}) b: [<class 'b.Plugin1'>, <class 'b.Plugin2'>] a: []
困惑点:b.py中打印的PluginBase.subclasses有内容,但a.py主程序里打印的却是空列表。即使把注册代码改成PluginBase.subclasses.append(cls)也没用,不过将a.py中的from b import Plugin1, Plugin2改成from b import *后,就能得到预期的结果:
__init_subclass__(<class 'b.Plugin1'>, **{}) __init_subclass__(<class 'b.Plugin2'>, **{}) b: [<class 'b.Plugin1'>, <class 'b.Plugin2'>] a: [<class 'b.Plugin1'>, <class 'b.Plugin2'>]
疑问
- 为何要使用
cls.subclasses.append而非PluginBase.subclasses.append? - 此场景下
from b import *与from b import Plugin1, Plugin2的差异是什么?
解答
问题1:cls.subclasses 和 PluginBase.subclasses的区别
核心原因是Python模块导入时的双重实例问题:
- 当你执行
python a.py时,a模块的__name__是__main__,此时定义的PluginBase是__main__.PluginBase。 - 而b模块导入a时,Python会加载一个独立的
a模块实例(命名为a,而非__main__),b里继承的是a.PluginBase,这和主程序里的__main__.PluginBase是完全不同的类对象。
不管你写cls.subclasses.append(cls)还是PluginBase.subclasses.append(cls),只要b里的子类继承的是a.PluginBase,操作的都是a.PluginBase.subclasses列表;而a主程序里打印的是__main__.PluginBase.subclasses,自然是空的。
cls.subclasses的写法其实更健壮:如果后续有子类自己定义了subclasses属性,会优先使用子类的;如果没有,才会向上查找父类的subclasses,这符合继承的属性查找逻辑。
问题2:两种导入方式的差异
差异的本质是是否覆盖了主程序中的PluginBase对象:
- 用
from b import Plugin1, Plugin2时,只导入了两个子类,主程序里的PluginBase还是原来的__main__.PluginBase,和b里的a.PluginBase不是同一个对象,所以打印的subclasses为空。 - 用
from b import *时,会导入b模块中所有不以_开头的名称,其中包括b模块里导入的a.PluginBase。这个PluginBase会直接覆盖主程序里原来的__main__.PluginBase,此时打印的PluginBase.subclasses其实是a.PluginBase.subclasses——也就是b模块中已经添加了子类的那个列表,所以显示有内容。
内容的提问来源于stack exchange,提问作者FriendFX
相关产品推荐
相关产品推荐

