Python中__init__被调用两次的原因、参数异常及解决方法咨询
Python类实例化中__init__重复调用的问题分析与解决
简化代码
class Base: __possible_types__ = {} def __new__(cls, *args, **kwargs) -> type: # pop the `__type__` argument because it # should be passed to the class `__init__` type_ = kwargs.pop("__type__", None) if type_ is not None: possible_type = cls.__possible_types__.get(type_) if possible_type is not None: return possible_type(*args, **kwargs) return super().__new__(cls) def __init__(self, *args, **kwargs) -> None: print(f"{self.__class__.__name__}.__init__", args, kwargs) def __init_subclass__(cls) -> None: __possible_types__ = {} for parent in cls.__mro__[1:]: if issubclass(parent, Base): parent.__possible_types__[cls.__name__] = cls class Parent(Base): pass class Child(Parent): pass class Adult(Parent): pass should_be_child = Parent( name="bob", __type__="Child" ) print(should_be_child)
执行输出
Child.__init__ () {'name': 'bob'} Child.__init__ () {'name': 'bob', '__type__': 'Child'} <__main__.Child object at 0x...>
问题解答
1. 为什么__init__会被调用两次?
两次调用的核心原因是构造流程的重复触发:
- 第一次:在
Base.__new__中,我们显式调用了possible_type(*args, **kwargs)(即Child(name="bob")),这个调用会完整执行Child的构造逻辑——调用__new__创建实例后,Python自动触发Child.__init__,这是第一次调用。 - 第二次:当
Base.__new__返回Child实例后,回到Parent(...)的构造逻辑。由于Child是Parent的子类,isinstance(Child实例, Parent)结果为True,Python会自动调用该实例的__init__方法,并传入Parent(...)调用时的原始参数,这就触发了第二次__init__调用。
2. 已弹出的字典项__type__为何会出现在第二次__init__调用的参数中?
Python在构造函数调用逻辑中,触发__init__时使用的是传递给构造函数的原始参数集合,而非__new__方法中修改后的kwargs。虽然我们在Base.__new__里执行了kwargs.pop("__type__"),但这只是修改了__new__函数内部的kwargs字典引用,Python触发第二次__init__时,仍然会传入最初调用Parent(...)时的完整参数,所以__type__会再次出现。
3. 该如何解决这个问题?
推荐从根源避免重复触发__init__,以下是两种可行方案:
方案1:直接调用目标类的__new__创建实例(推荐)
修改Base.__new__,不直接调用目标类的构造函数(避免触发第一次__init__),而是直接调用目标类的__new__方法创建实例,让Python在后续逻辑中统一触发一次__init__:
class Base: __possible_types__ = {} def __new__(cls, *args, **kwargs) -> type: type_ = kwargs.pop("__type__", None) if type_ is not None: possible_type = cls.__possible_types__.get(type_) if possible_type is not None: # 直接调用目标类的__new__创建实例,不触发完整构造流程 obj = possible_type.__new__(possible_type, *args, **kwargs) return obj return super().__new__(cls) # 其余代码不变
修改后执行输出:
Child.__init__ () {'name': 'bob'} <__main__.Child object at 0x...>
此时__init__仅调用一次,且参数中没有__type__(因为kwargs.pop已经修改了原始字典)。
方案2:给__init__添加重复执行判断
如果无法修改__new__逻辑,可以给实例添加标记,避免__init__重复执行核心逻辑:
class Base: # 其余代码不变 def __init__(self, *args, **kwargs) -> None: # 检查是否已经初始化过 if hasattr(self, "_initialized"): return print(f"{self.__class__.__name__}.__init__", args, kwargs) self._initialized = True
这种方案通过标记过滤重复调用,虽然不会阻止__init__被调用两次,但会跳过重复的逻辑执行。
内容的提问来源于stack exchange,提问作者dsal3389
相关产品推荐
相关产品推荐

