Mixin类的__init__函数不会自动调用吗?

72

我想使用Mixin为我的子类添加一些初始化功能,这些子类都继承自不同的API基类。具体来说,我想创建多个不同的子类,它们都继承自API提供的不同基类之一,以及一个Mixin,该Mixin始终以相同的方式执行Mixin初始化代码,而无需复制代码。但是,似乎Mixin类的__init__函数永远不会被调用,除非我在子类的__init__函数中显式调用它,这不太理想。我已经建立了一个简单的测试案例:

class APIBaseClassOne(object):
    def __init__(self, *args, **kwargs):
        print (" base ")

class SomeMixin(object):
    def __init__(self, *args, **kwargs):
        print (" mixin before ")
        super(SomeMixin, self).__init__(*args, **kwargs)
        print (" mixin after ")

class MyClass(APIBaseClassOne):
    pass

class MixedClass(MyClass, SomeMixin):
    pass

如下输出所示,Mixin函数的init从未被调用:

>>> import test
>>> test.MixedClass()
 base
<test.MixedClass object at 0x1004cc850>

有没有一种方法可以让一个Mixin中的初始化函数被调用,而不需要显式地在每个子类中调用它?(也就是说,不需要在每个类中都像这样做:)

class MixedClass(MyClass, SomeMixin):
    def __init__(*args, **kwargs):
        SomeMixin.__init__(self, *args, **kwargs)
        MyClass.__init__(self, *args, **kwargs) 

顺便说一句,如果我所有的子类都从同一个基类继承,我意识到我可以创建一个新的中间类,该类从基类和混合类中继承,并以此方式保持DRY。然而,它们从具有共同功能的不同基类继承(确切地说是Django字段类)。


1
通常情况下,使用未设计为多重继承的基类进行多重继承是一个不好的想法。混合类通常是一起设计的,而混入任意类会产生混乱。无论如何,如果两个基类都有一个__init__方法,解释器应该如何知道调用哪一个或以什么顺序调用它们呢? - André Caron
2
@André Caron:它可以像C++一样确定顺序,其中基类按声明顺序初始化。 - martineau
4个回答

61

抱歉我晚来了,但是

class MixedClass2(SomeMixin, MyClass):
    pass

>>> m = MixedClass2()
 mixin before 
 base 
 mixin after

@Ignacio所说的模式被称为协同多重继承,非常不错。但是如果一个基类不感兴趣合作,那么将其放在第二个基类位置,而将混入放在第一位。在Python的MRO遵循下,混入的__init__()(和任何其他定义)将在基类之前检查。

这应该解决了一般问题,但我不确定它是否适用于您的特定用法。具有自定义元类(例如Django模型)或具有奇怪装饰符(例如@martineau的答案 ;) 的基类可能会做出疯狂的事情。


2
即使有些晚了,这也很好地概括了问题(尽管其他答案提供了一些非常好的教学) - 最终,在进行任何多重继承冒险之前,理解Python MRO似乎是至关重要的,否则会发生意想不到的事情。谢谢! - B Robster
欢迎!确实,很不幸MRO被呈现为一个高级话题:/ - Matt Luongo

34

即使基类是object的子类,也要让基类调用super().__init__()。这样所有的__init__()方法都将被运行。

class BaseClassOne(object):
    def __init__(self, *args, **kwargs):
        super(BaseClassOne, self).__init__(*args, **kwargs)
        print (" base ")

谢谢!我没有意识到调用super会导致所有__init__()方法运行。不幸的是,BaseClassOne是一个API(Django)提供的基类(我已经更新了我的问题以反映这一点),但这可能会让我朝着正确的方向开始。 - B Robster
11
它不会导致所有的都运行,但会导致下一个运行,即使它在混入中。 - Ignacio Vazquez-Abrams
1
@kindall:我认为很多人都忽略了一个重点,那就是OP无法更改BaseClassOne,因为它是API的一部分,而他们无法控制。 - martineau
2
@martineau:这个事实是在我回答之后才被揭示出来的。这并不意味着我的回答是错误的,只是对于这种情况来说不太相关了。 - Ignacio Vazquez-Abrams
@kindall:我尝试在OP的MyClassMixedClass中都添加了一个带有super(...).__init__调用的__init__,但是当我实例化一个MixedClass对象时,SomeMixin__init__仍然没有被调用——所以也许我误解了你的建议。 - martineau
显示剩余5条评论

26

Python不会自动调用一个类的父类(们)的__init__方法,但是有可能使其自动发生。一种方法是为您的混合类定义元类,该元类创建或扩展混合类的__init__方法,以便按照它们被列出的顺序调用所有列出的基类的__init__函数。

第二种方法是使用类装饰器,如下面的编辑部分所示。

使用元类:

class APIBaseClassOne(object):  # API class (can't be changed)
    def __init__(self, *args, **kwargs):
        print('  APIBaseClassOne.__init__()')

class SomeMixin(object):
    def __init__(self, *args, **kwargs):
        print('  SomeMixin.__init__()')

class MixedClassMeta(type):
    def __new__(cls, name, bases, classdict):
        classinit = classdict.get('__init__')  # Possibly None.

        # Define an __init__ function for the new class.
        def __init__(self, *args, **kwargs):
            # Call the __init__ functions of all the bases.
            for base in type(self).__bases__:
                base.__init__(self, *args, **kwargs)
            # Also call any __init__ function that was in the new class.
            if classinit:
                classinit(self, *args, **kwargs)

        # Add the local function to the new class.
        classdict['__init__'] = __init__
        return type.__new__(cls, name, bases, classdict)

class MixedClass(APIBaseClassOne, SomeMixin):
    __metaclass__ = MixedClassMeta  # important
    # If exists, called after the __init__'s of all the direct bases.
    def __init__(self, *args, **kwargs):
        print('  MixedClass.__init__()')

print('MixedClass():')
MixedClass()

输出:

MixedClass():
  APIBaseClassOne.__init__()
  SomeMixin.__init__()
  MixedClass.__init__()

编辑

以下是使用类装饰器实现相同功能的方法(需要Python 2.6+):

class APIBaseClassOne(object):  # API class (can't be changed)
    def __init__(self, *args, **kwargs):
        print('  APIBaseClassOne.__init__()')

class SomeMixin(object):
    def __init__(self, *args, **kwargs):
        print('  SomeMixin.__init__()')

def mixedomatic(cls):
    """ Mixed-in class decorator. """
    classinit = cls.__dict__.get('__init__')  # Possibly None.

    # Define an __init__ function for the class.
    def __init__(self, *args, **kwargs):
        # Call the __init__ functions of all the bases.
        for base in cls.__bases__:
            base.__init__(self, *args, **kwargs)
        # Also call any __init__ function that was in the class.
        if classinit:
            classinit(self, *args, **kwargs)

    # Make the local function the class's __init__.
    setattr(cls, '__init__', __init__)
    return cls

@mixedomatic
class MixedClass(APIBaseClassOne, SomeMixin):
    # If exists, called after the __init__'s of all the direct base classes.
    def __init__(self, *args, **kwargs):
        print('  MixedClass.__init__()')

print('MixedClass():')
MixedClass()

注意事项

对于Python版本小于2.6,需要在类定义之后使用MixedClass = mixedomatic(MixedClass)

在Python 3中,指定元类的语法有所不同,因此需要使用以下语法:

class MixedClass(APIBaseClassOne, SomeMixin):
    __metaclass__ = MixedClassMeta  # important

如上所示,您需要使用以下内容:
class MixedClass(APIBaseClassOne, SomeMixin, metaclass=MixedClassMeta):

类装饰器版本在两个版本中都可以原封不动地使用。


1
感谢您的演示。虽然其他答案中的讨论让我重新考虑了是否从这里开始是一个好主意,但这个实现方法是有效的,我也学到了很多。 - B Robster
1
@Ben:是的,混合类有些争议,甚至被一些人认为是有害的,而且在Python中使用它们有点复杂,因为它不会默认调用基类构造函数(不像C++那样)。然而,在像你这样无法修改API类的情况下,它们的使用可能是合理的,但你可能需要考虑更多替代方案来实现你所需的功能,比如子类化或包装API类。另一个可能性是将其实例作为你自己类的属性。 - martineau
@martineau 实际上,现在我想起来我有以下问题:@mixedomatic class SomeCls(SomeMixin, SomeBase): passSomeCls 没有 __init__,但使用 getattr 会产生一个 init,因为 SomeMixin 和/或 SomeBase 可能有一个 init。我的建议将正确地识别出 SomeCls 没有 init。我在上面提到的 optimizers.py 中使用了它。 - Al Conrad
@AlConrad:好的,现在我明白你的意思了。实际上,在类装饰器的情况下,getattr()返回的内容与if-in语句不同,因为这些语句是在已经创建了一个初始未修改版本的类之后执行的——getattr()遵循该临时类的MRO,并返回继承层次结构中第一个匹配项中的__init__(这里是第一个基类的__init__())... - martineau
这与元类的操作方式不同,因为它们是在使用它们进行单个类构造过程的一部分,所以它们不会遇到这个问题。感谢指出装饰器问题并建议修复方法。 - martineau
显示剩余8条评论

0
我们可以从所有答案中学到很多东西。 但我认为只有Ignacio的答案在这里是正确的。
换句话说,简单地说:
如果你重写了某个方法,请务必在其中添加super()调用!!! 任何没有super()调用的方法都会破坏随后遵循的类/混合类的运行!!!
因此,对于如果我们创建类的情况,Ignacio的答案是正确的。
问题在于第三方作者编写的类以这种方式存在缺陷。 我在使用Django Rest Framework时发现了这个stackoverflow问题。
class FinalViewSet(viewsets.ModelViewSet, MyMixin)

将会失败(MyMixin的__init__不会运行),而

class FinalViewSet(MyMixin, viewsets.ModelViewSet)

会使事情正确。

所以让我们转到 viewsets.ModelViewSet 的源代码...

... 然后我们将发现 django.views.generic.base.View 有错误的 __init__ 方法。

因此,在这种情况下,问题不在 DRF 的作者身上,而是在 Django 的作者身上。


你应该把这个问题提出来,虽然有可能这个库里面有一些黑魔法在起作用,导致它破坏了一些东西。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接