Python:当父类接受不同参数时,正确的初始化方法是什么?

25 投票
2 回答
8289 浏览
提问于 2025-04-16 06:08

如果我有三个这样的类:

class BaseClass(object):
    def __init__(self, base_arg, base_arg2=None):
        ...

class MixinClass(object):
    def __init__(self, mixin_arg):
        ...

class ChildClass(BaseClass, MixinClass):
    def __init__(self, base_arg, mixin_arg, base_arg2=None):
        ???

那么正确的方式来初始化 MixinClassBaseClass 是什么呢?

看起来我不能使用 super,因为 MixinClassBaseClass 接受的参数不一样……而且如果我分别调用 MixinClass.__init__(...)BaseClass.__init__(...),可能会导致一个叫做“菱形继承”的问题,而 super 就是为了避免这个问题而设计的。

2 个回答

3

为了进一步解释@chris-b的正确回答,下面是一些基于提问者使用场景的例子,以及我已经尝试过的一种模式(虽然大多数情况下都没成功,至少在代码美观方面是这样)。

回顾

简单来说,如果你在每个你写的类的init方法中调用super.__init__,那么在使用多重继承时,Python会按照方法解析顺序(MRO)来依次调用所有类的初始化方法。super.__init__的作用是调用parent.__init__,并让parent.__init__去调用它的所有兄弟类的初始化方法。

因此,对于一个简单的class C(A, B)B.__init__只有在A.__init__本身调用了super.__init__的情况下才会被调用,即使C.__init__使用了super.__init__

另一种方法是手动调用你想要的初始化方法,比如在C.__init__中调用A.__init__(self)B.__init__(self);这种方法的缺点是,它可能会破坏未来继承的类,这些类调用super并期望所有父类的初始化方法也被调用。你必须了解各个父类的初始化方法的作用。

因此,人们可能会认为一直使用super.__init__是正确的做法;但正如提问者所说,当不同的init方法期望不同的参数时,这种“魔法”调用链就会中断(这在使用混入模式时很常见!)。

更多信息可以在这个链接中找到。

有没有完美的解决方案?

不幸的是,似乎在Python中使用多重继承(和混入模式)需要对多个层次的情况有一定的了解;

即使尝试通过接受*args**kwargs来规划扩展情况,并调用super.__init__传递所有参数,也会失败,因为对象的init()只接受一个参数(self)!

这在下面的第一个例子中有说明。

我使用的一个非常丑陋的黑客方法是将对super.init的调用放在try except块中,如下所示:

try:
    super(ThisClass, self).__init__(*args, arg1=arg1, arg2=arg2, **kwargs)
except TypeError as e:
    # let's hope this TypeError is due to the arguments for object...
    super(ThisClass, self).__init__()

这似乎有效,但真的很丑陋。

我做了一个gist:https://gist.github.com/stefanocrosta/1d113a6a0c79f853c30a64afc4e8ba0a

但以防万一,这里是例子:

完整示例 1

class BaseClass(object):
    def __init__(self, base_arg, base_arg2=None, *args, **kwargs):
        print "\tBaseClass: {}, {}".format(base_arg, base_arg2)
        super(BaseClass, self).__init__(*args, base_arg=base_arg, base_arg2=base_arg2, **kwargs)

class MixinClass(object):
    def __init__(self, mixin_arg, *args, **kwargs):
        print "\tMixinClass: {}".format(mixin_arg)
        super(MixinClass, self).__init__()

class MixinClassB(object):
    def __init__(self, mixin_arg, *args, **kwargs):
        print "\tMixinClassB: {}".format(mixin_arg)
        super(MixinClassB, self).__init__(*args, mixin_arg=mixin_arg, **kwargs)

class ChildClassA(BaseClass, MixinClass):
    """
    Let's make it work for this case
    """
    def __init__(self, base_arg, mixin_arg, base_arg2=None):
        print "Initializing {}: base_arg: {} mixin_arg: {} base_arg2: {}".format(
            self.__class__.__name__, base_arg, mixin_arg, base_arg2)
        super(ChildClassA, self).__init__(base_arg=base_arg, mixin_arg=mixin_arg, base_arg2=base_arg2)

class ChildClassB(BaseClass, MixinClass):
    """
    Same as above, but without specifying the super.__init__ arguments names
    """
    def __init__(self, base_arg, mixin_arg, base_arg2=None):
        print "Initializing {}: base_arg: {} mixin_arg: {} base_arg2: {}".format(
            self.__class__.__name__, base_arg, mixin_arg, base_arg2)
        # If you don't specify the name of the arguments, you need to use the correct order of course:
        super(ChildClassB, self).__init__(base_arg, base_arg2, mixin_arg)

class ChildClassC(BaseClass, MixinClassB, MixinClass):
    """
    Now let's simply add another mixin: before...
    """
    def __init__(self, base_arg, mixin_arg, base_arg2=None):
        print "Initializing {}: base_arg: {} mixin_arg: {} base_arg2: {}".format(
            self.__class__.__name__, base_arg, mixin_arg, base_arg2)
        super(ChildClassC, self).__init__(base_arg=base_arg, mixin_arg=mixin_arg, base_arg2=base_arg2)

class ChildClassD(BaseClass, MixinClass, MixinClassB):
    """
    Now let's simply add another mixin: ..and after
    """
    def __init__(self, base_arg, mixin_arg, base_arg2=None):
        print "Initializing {}: base_arg: {} mixin_arg: {} base_arg2: {}".format(
            self.__class__.__name__, base_arg, mixin_arg, base_arg2)
        super(ChildClassD, self).__init__(base_arg=base_arg, mixin_arg=mixin_arg, base_arg2=base_arg2)        

childA = ChildClassA(1, 3, 2)  # note the order of the arguments - the mixin arg is interleaved
childB = ChildClassB(1, 3, 2)
childC = ChildClassC(1, 3, 2)
childD = ChildClassD(1, 3, 2)

完整示例 2:

class BaseClass(object):
    def __init__(self, base_arg, base_arg2=None, *args, **kwargs):
        print "\tBaseClass: {}, {}".format(base_arg, base_arg2)
        try:
            super(BaseClass, self).__init__(*args, base_arg=base_arg, base_arg2=base_arg2, **kwargs)
        except:
            super(BaseClass, self).__init__()

class MixinClass(object):
    def __init__(self, mixin_arg, *args, **kwargs):
        print "\tMixinClass: {}".format(mixin_arg)
        try:
            super(MixinClass, self).__init__(*args, mixin_arg=mixin_arg, **kwargs)
        except:
            super(MixinClass, self).__init__()

class MixinClassB(object):
    def __init__(self, mixin_arg, *args, **kwargs):
        print "\tMixinClassB: {}".format(mixin_arg)
        try:
            super(MixinClassB, self).__init__(*args, mixin_arg=mixin_arg, **kwargs)
        except:
            super(MixinClassB, self).__init__()

class ChildClassA(BaseClass, MixinClass):
    """
    Let's make it work for this case
    """
    def __init__(self, base_arg, mixin_arg, base_arg2=None):
        print "Initializing {}: base_arg: {} mixin_arg: {} base_arg2: {}".format(
            self.__class__.__name__, base_arg, mixin_arg, base_arg2)
        super(ChildClassA, self).__init__(base_arg=base_arg, mixin_arg=mixin_arg, base_arg2=base_arg2)        


class ChildClassC(BaseClass, MixinClassB, MixinClass):
    """
    Now let's simply add another mixin: before...
    """
    def __init__(self, base_arg, mixin_arg, base_arg2=None):
        print "Initializing {}: base_arg: {} mixin_arg: {} base_arg2: {}".format(
            self.__class__.__name__, base_arg, mixin_arg, base_arg2)
        super(ChildClassC, self).__init__(base_arg=base_arg, mixin_arg=mixin_arg, base_arg2=base_arg2)

class ChildClassD(BaseClass, MixinClass, MixinClassB):
    """
    Now let's simply add another mixin: ..and after
    """
    def __init__(self, base_arg, mixin_arg, base_arg2=None):
        print "Initializing {}: base_arg: {} mixin_arg: {} base_arg2: {}".format(
            self.__class__.__name__, base_arg, mixin_arg, base_arg2)
        super(ChildClassD, self).__init__(base_arg=base_arg, mixin_arg=mixin_arg, base_arg2=base_arg2)        

try:
    base = BaseClass(1, 2)
except Exception as e:
    print "Failed because object.__init__ does not expect any argument ({})".format(e)
childA = ChildClassA(1, 3, 2)  # note the order of the arguments - the mixin arg is interleaved
childC = ChildClassC(1, 3, 2)
childD = ChildClassD(1, 3, 2)
16

简单来说,在Python中,不能 安全地支持这种类型的继承。幸运的是,你几乎不需要这样做,因为大多数方法并不关心某个东西是什么,只关心它是否支持特定的接口。最好的办法是使用组合或聚合:让你的类继承其中一个父类,同时包含对另一个类实例的引用。只要你的类支持第二个类的接口(并将消息转发给包含的实例),这通常就能正常工作。

在上面的例子中(两个类都继承自 object),你可以(可能)安全地同时继承这两个类,并使用 MixinClass.__init__BaseClass.__init__ 来调用它们的构造函数。但要注意,如果父类在它们的构造函数中调用了 super,这样做就不安全。一个好的经验法则是:如果父类使用 super,那么你也用 super;如果父类使用 __init__,那么你就用 __init__。希望你永远不要被迫从两个选择了不同初始化方法的类中继承。

撰写回答