【问题标题】:How does Python's super() work with multiple inheritance?Python 的 super() 如何与多重继承一起工作?
【发布时间】:2018-01-27 09:03:04
【问题描述】:

我是 Python 面向对象编程的新手,但遇到了麻烦 了解super() 函数(新样式类),尤其是在涉及多重继承时。

例如,如果你有类似的东西:

class First(object):
    def __init__(self):
        print "first"

class Second(object):
    def __init__(self):
        print "second"

class Third(First, Second):
    def __init__(self):
        super(Third, self).__init__()
        print "that's it"

我不明白的是:Third() 类会继承这两个构造函数方法吗?如果是,那么将使用super() 运行哪一个,为什么?

如果你想运行另一个呢?我知道这与 Python 方法解析顺序有关 (MRO)。

【问题讨论】:

  • 事实上,多重继承是唯一可以使用super() 的情况。我不建议将它与使用线性继承的类一起使用,这只是无用的开销。
  • @Bachsau 在技术上是正确的,因为它的开销很小,但 super() 更符合 Python 风格,并且允许随着时间的推移重构和更改代码。除非你真的需要一个命名类特定的方法,否则使用 super()。
  • super() 的另一个问题是,它强制每个子类也使用它,而当不使用super() 时,每个子类都可以自己决定。如果使用它的开发人员不知道 super() 或不知道它已被使用,则可能会出现难以追踪的 mro 问题。
  • 我发现这里的每个答案几乎都以一种或另一种方式令人困惑。您实际上会转而参考here
  • @Bachsau 使用super 使您的类 用于多重继承,无论它是否使用多重继承。 (但您的第二点是有效的;super 的使用确实是您的类的公共接口的一部分,而不仅仅是实现细节。)

标签: python multiple-inheritance


【解决方案1】:

总体

假设所有内容都源自object(如果不是,则您只能靠自己),Python 会根据您的类继承树计算方法解析顺序 (MRO)。 MRO 满足 3 个属性:

  • 一个班级的孩子比他们的父母更早
  • 左父母先于右父母
  • 一个类在 MRO 中只出现一次

如果不存在这样的排序,Python 会出错。其内部工作是类祖先的 C3 线性化。在此处阅读所有相关信息:https://www.python.org/download/releases/2.3/mro/

因此,在下面的两个示例中,它都是:

  1. 孩子
  2. 父母

调用方法时,MRO 中该方法的第一次出现就是被调用的方法。任何未实现该方法的类都将被跳过。在该方法中对super 的任何调用都将调用该方法在 MRO 中的下一次出现。因此,在继承中放置类的顺序以及在方法中调用super 的位置都很重要。

请注意,您可以使用__mro__ 方法在python 中查看MRO。 Child.__mro__ 在以下任何示例中都会返回:

(__main__.Child, __main__.Left, __main__.Right, __main__.Parent, object)

示例

以下所有示例都具有如下类的菱形继承:

    Parent
    /   \
   /     \
Left    Right
   \     /
    \   /
    Child

super 在每个方法中都放在首位

class Parent(object):
    def __init__(self):
        super(Parent, self).__init__()
        print("parent")

class Left(Parent):
    def __init__(self):
        super(Left, self).__init__()
        print("left")

class Right(Parent):
    def __init__(self):
        super(Right, self).__init__()
        print("right")

class Child(Left, Right):
    def __init__(self):
        super(Child, self).__init__()
        print("child")

Child() 输出:

parent
right
left
child
    

super 在每个方法的最后一个

class Parent(object):
    def __init__(self):
        print("parent")
        super(Parent, self).__init__()

class Left(Parent):
    def __init__(self):
        print("left")
        super(Left, self).__init__()

class Right(Parent):
    def __init__(self):
        print("right")
        super(Right, self).__init__()

class Child(Left, Right):
    def __init__(self):
        print("child")
        super(Child, self).__init__()

Child() 输出:

child
left
right
parent

当不是所有类都调用super

如果不是继承链调用super 中的所有类,继承顺序最重要。例如,如果Left 没有调用super,那么RightParent 上的方法永远不会被调用:

class Parent(object):
    def __init__(self):
        print("parent")
        super(Parent, self).__init__()

class Left(Parent):
    def __init__(self):
        print("left")

class Right(Parent):
    def __init__(self):
        print("right")
        super(Right, self).__init__()

class Child(Left, Right):
    def __init__(self):
        print("child")
        super(Child, self).__init__()

Child() 输出:

child
left

或者,如果Right 不调用superParent 仍然会被跳过:

class Parent(object):
    def __init__(self):
        print("parent")
        super(Parent, self).__init__()

class Left(Parent):
    def __init__(self):
        print("left")
        super(Left, self).__init__()

class Right(Parent):
    def __init__(self):
        print("right")

class Child(Left, Right):
    def __init__(self):
        print("child")
        super(Child, self).__init__()

这里,Child() 输出:

child
left
right

【讨论】:

  • 我看到您可以使用来自Childsuper() 访问Left。假设我想从Child 内部访问Right。有没有办法使用 super 从Child 访问Right?还是应该直接从super内部调用Right
  • @alpha_989 如果您只想访问特定类的方法,您应该直接引用该类而不是使用 super。 Super 是关于遵循继承链,而不是获取特定类的方法。
  • 感谢您明确提及“一个类在 MRO 中只出现一次”。这解决了我的问题。现在我终于明白了多重继承是如何工作的。有人需要提到 MRO 的属性!
【解决方案2】:

如果您尝试继承的每个类都有自己的位置参数作为其 init,只需调用每个类自己的 init 方法,如果尝试从多个对象继承,请不要使用 super。

class A():
    def __init__(self, x):
        self.x = x

class B():
    def __init__(self, y, z):
        self.y = y
        self.z = z

class C(A, B):
    def __init__(self, x, y, z):
        A.__init__(self, x)
        B.__init__(self, y, z)

>>> c = C(1,2,3)
>>>c.x, c.y, c.z 
(1, 2, 3)

【讨论】:

    【解决方案3】:

    考虑子 AB,其中父 AB 在其构造函数中具有关键字参数。

      A    B
       \  /
        AB
    

    要初始化AB,您需要显式调用父类构造函数,而不是使用super()

    例子:

    class A():
        def __init__(self, a="a"):
            self.a = a
            print(f"a={a}")
        
        def A_method(self):
            print(f"A_method: {self.a}")
    
    class B():
        def __init__(self, b="b"):
            self.b = b
            print(f"b={b}")
        
        def B_method(self):
            print(f"B_method: {self.b}")
        
        def magical_AB_method(self):
            print(f"magical_AB_method: {self.a}, {self.b}")
    
    class AB(A,B):
        def __init__(self, a="A", b="B"):
            # super().__init__(a=a, b=b) # fails!
            A.__init__(self, a=a)
            B.__init__(self, b=b)
            self.A_method()
            self.B_method()
            self.magical_AB_method()
    
    
    A()
    >>> a=a
    
    B()
    >>> b=b
    
    AB()
    >>> a=A
    >>> b=B
    >>> A_method: A
    >>> B_method: B
    

    为了证明两个父级合并为子级,请考虑在类B 中定义的magical_AB_method。当从B 的实例调用时,该方法将失败,因为它无法访问A 中的成员变量。但是,当从子 AB 的实例调用时,此方法有效,因为它从 A 继承了所需的成员变量。

    B().magical_AB_method()
    >>> AttributeError: 'B' object has no attribute 'a'
    
    AB().magical_AB_method()
    >>> magical_AB_method: A, B
    

    【讨论】:

      【解决方案4】:

      Guido 本人在他的博文Method Resolution Order 中详细介绍了这一点(包括之前的两次尝试)。

      在您的示例中,Third() 将调用 First.__init__。 Python 在类的父类中查找每个属性,因为它们从左到右列出。在这种情况下,我们正在寻找__init__。所以,如果你定义

      class Third(First, Second):
          ...
      

      Python 将首先查看First,如果First 没有该属性,那么它将查看Second

      当继承开始跨越路径时,这种情况会变得更加复杂(例如,如果 First 继承自 Second)。阅读上面的链接了解更多详细信息,但简而言之,Python 将尝试保持每个类出现在继承列表中的顺序,从子类本身开始。

      例如,如果您有:

      class First(object):
          def __init__(self):
              print "first"
      
      class Second(First):
          def __init__(self):
              print "second"
      
      class Third(First):
          def __init__(self):
              print "third"
      
      class Fourth(Second, Third):
          def __init__(self):
              super(Fourth, self).__init__()
              print "that's it"
      

      MRO 将是 [Fourth, Second, Third, First].

      顺便说一句:如果 Python 找不到一致的方法解析顺序,它会引发异常,而不是退回到可能让用户感到惊讶的行为。

      模棱两可的 MRO 示例:

      class First(object):
          def __init__(self):
              print "first"
              
      class Second(First):
          def __init__(self):
              print "second"
      
      class Third(First, Second):
          def __init__(self):
              print "third"
      

      Third 的 MRO 应该是 [First, Second] 还是 [Second, First]?没有明显的期望,Python 会报错:

      TypeError: Error when calling the metaclass bases
          Cannot create a consistent method resolution order (MRO) for bases Second, First
      

      为什么上面的例子缺少super() 调用?这些示例的重点是展示如何构建 MRO。它们旨在打印"first\nsecond\third" 或其他任何内容。您可以——当然也应该使用该示例,添加super() 调用,看看会发生什么,并更深入地了解 Python 的继承模型。但我的目标是保持简单并展示 MRO 是如何构建的。它是按照我解释的那样构建的:

      >>> Fourth.__mro__
      (<class '__main__.Fourth'>,
       <class '__main__.Second'>, <class '__main__.Third'>,
       <class '__main__.First'>,
       <type 'object'>)
      

      【讨论】:

      • 当您开始在 First、Second 和 Third [pastebin.com/ezTyZ5Wa] 中调用 super() 时,它会变得更有趣(并且可以说更令人困惑)。
      • 我认为第一堂课缺少超级电话是这个答案的一个很大的问题;不讨论如何/为什么失去对问题的重要批判性理解。
      • 这个答案是完全错误的。如果没有父级的 super() 调用,什么都不会发生。 @lifeless 的答案是正确的。
      • @Cerin 这个例子的重点是展示如何构建 MRO。该示例不打算打印“first\nsecond\third”或其他任何内容。而且 MRO 确实是正确的:Fourth.__mro__ == (main.Fourth'>, main.Second'>, main.Third'>, main.First'>, )
      • 据我所知,这个答案缺少 OP 的一个问题,即“如果你想运行另一个问题怎么办?”。我想看看这个问题的答案。我们是否应该明确地命名基类?
      【解决方案5】:

      关于@calfzhou's comment,你可以照常使用**kwargs

      Online running example

      class A(object):
        def __init__(self, a, *args, **kwargs):
          print("A", a)
      
      class B(A):
        def __init__(self, b, *args, **kwargs):
          super(B, self).__init__(*args, **kwargs)
          print("B", b)
      
      class A1(A):
        def __init__(self, a1, *args, **kwargs):
          super(A1, self).__init__(*args, **kwargs)
          print("A1", a1)
      
      class B1(A1, B):
        def __init__(self, b1, *args, **kwargs):
          super(B1, self).__init__(*args, **kwargs)
          print("B1", b1)
      
      
      B1(a1=6, b1=5, b="hello", a=None)
      

      结果:

      A None
      B hello
      A1 6
      B1 5
      

      您也可以按位置使用它们:

      B1(5, 6, b="hello", a=None)
      

      但是您必须记住 MRO,这真的很混乱。您可以使用keyword-only parameters 来避免这种情况:

      class A(object):
        def __init__(self, *args, a, **kwargs):
          print("A", a)
      

      等等。

      我可能有点烦人,但我注意到人们在重写方法时每次都忘记使用 *args**kwargs,而这是对这些“神奇变量”的少数真正有用和理智的使用之一。

      【讨论】:

      • 哇,这真的很难看。很遗憾你不能只说你想调用哪个特定的超类。尽管如此,这让我更有动力使用组合并避免像瘟疫一样的多重继承。
      • @TomBusby:嗯,我同意。理论上,您可以定义__new__ 并在其中调用B.__new__(),例如,在__init__ 中调用B.__init__()。但这太复杂了……
      【解决方案6】:

      考虑调用从子类调用的super().Foo()方法解析顺序 (MRO) 方法是解析方法调用的顺序。

      案例一:单一继承

      在这种情况下,super().Foo() 将在层次结构中向上搜索,并会考虑最接近的实现,如果找到,则引发异常。 “is a”关系在任何访问过的子类与其在层次结构中向上的超类之间始终为 True。但这个故事在多重继承中并不总是一样。

      案例 2:多重继承

      在这里,在搜索 super().Foo() 实现时,层次结构中的每个访问的类可能有也可能没有 is a 关系。考虑以下示例:

      class A(object): pass
      class B(object): pass
      class C(A): pass
      class D(A): pass
      class E(C, D): pass
      class F(B): pass
      class G(B): pass
      class H(F, G): pass
      class I(E, H): pass
      

      这里,I 是层次结构中最低的类。 I 的层次结构图和 MRO 将是

      (红色数字表示 MRO)

      MRO 是 I E C D A H F G B object

      请注意,只有当所有继承自它的子类都被访问过时,才会访问一个类X(即,您永远不应该访问一个有箭头从其下方的类进入的类您还没有访问过)。

      这里,请注意,在访问类 C 之后,D 被访问,尽管 CD 在它们之间没有 关系(但两者都有 A )。这就是super() 与单继承不同的地方。

      考虑一个稍微复杂一点的例子:

      (红色数字表示 MRO)

      MRO 是 I E C H D A F G B object

      在这种情况下,我们从IEC。下一步将是A,但我们还没有访问D,它是A 的子类。但是,我们不能访问D,因为我们还没有访问HD 的子类。叶子H 作为下一节参观。请记住,如果可能,我们会尝试在层次结构中上升,因此我们访问其最左边的超类D。在D 之后,我们访问A,但我们不能反对,因为我们还没有访问FGB。这些课程按顺序完善了 I 的 MRO。

      请注意,任何课程在 MRO 中都不能出现多次。

      这就是 super() 在继承层次结构中的查找方式。

      资源致谢:Richard L Halterman Python 编程基础

      【讨论】:

        【解决方案7】:

        发布此答案以供我将来参考。

        Python 多重继承应使用菱形模型,并且模型中的函数签名不应更改。

            A
           / \
          B   C
           \ /
            D
        

        示例代码 sn-p 将是 ;-

        class A:
            def __init__(self, name=None):
                #  this is the head of the diamond, no need to call super() here
                self.name = name
        
        class B(A):
            def __init__(self, param1='hello', **kwargs):
                super().__init__(**kwargs)
                self.param1 = param1
        
        class C(A):
            def __init__(self, param2='bye', **kwargs):
                super().__init__(**kwargs)
                self.param2 = param2
        
        class D(B, C):
            def __init__(self, works='fine', **kwargs):
                super().__init__(**kwargs)
                print(f"{works=}, {self.param1=}, {self.param2=}, {self.name=}")
        
        d = D(name='Testing')
        

        这里的A类是object

        【讨论】:

        • A 应该调用__init__A 没有“发明”方法 __init__,因此它不能假设其他一些类可能在其 MRO 的早期有 A__init__ 方法不(也不应该)调用super().__init__ 的唯一类是object
        • 是的。这就是我写A的原因是object也许我认为,我应该写class A (object) :来代替
        • A 不能是 object,如果您要向其 __init__ 添加参数。
        【解决方案8】:

        在 python 3.5+ 中,继承看起来可以预测并且对我来说非常好。 请看这段代码:

        class Base(object):
          def foo(self):
            print("    Base(): entering")
            print("    Base(): exiting")
        
        
        class First(Base):
          def foo(self):
            print("   First(): entering Will call Second now")
            super().foo()
            print("   First(): exiting")
        
        
        class Second(Base):
          def foo(self):
            print("  Second(): entering")
            super().foo()
            print("  Second(): exiting")
        
        
        class Third(First, Second):
          def foo(self):
            print(" Third(): entering")
            super().foo()
            print(" Third(): exiting")
        
        
        class Fourth(Third):
          def foo(self):
            print("Fourth(): entering")
            super().foo()
            print("Fourth(): exiting")
        
        Fourth().foo()
        print(Fourth.__mro__)
        

        输出:

        Fourth(): entering
         Third(): entering
           First(): entering Will call Second now
          Second(): entering
            Base(): entering
            Base(): exiting
          Second(): exiting
           First(): exiting
         Third(): exiting
        Fourth(): exiting
        (<class '__main__.Fourth'>, <class '__main__.Third'>, <class '__main__.First'>, <class '__main__.Second'>, <class '__main__.Base'>, <class 'object'>)
        

        如您所见,它为每个继承的链调用 foo 一次,其顺序与继承的顺序相同。您可以致电.mro 获得该订单:

        第四 -> 第三 -> 第一个 -> 第二个 -> 基础 -> 对象

        【讨论】:

        • 为什么它没有遵循以下顺序:第四 -> 第三 -> 第一 -> 基地 -> 第二 -> 基地?每次方法调用super时,都会去父类,为什么在“First”类的情况下没有发生这种情况?
        • @lousycoder 这只是因为python阻止调用两次“Base”
        • 我在哪里可以获得有关此的更多详细信息?
        • @lousycoder 您可以通过搜索“方法解析顺序”(MRO)来了解它,或者只需查看该链接:en.wikipedia.org/wiki/C3_linearization
        【解决方案9】:

        我想详细说明一下the answer by lifeless,因为当我开始阅读有关如何在 Python 的多重继承层次结构中使用 super() 时,我没有立即明白。

        你需要了解的是super(MyClass, self).__init__()提供了next__init__方法根据使用的方法解析排序(MRO)算法在完整继承层次的上下文中 /em>。

        最后一部分是理解的关键。让我们再次考虑这个例子:

        #!/usr/bin/env python2
        
        class First(object):
          def __init__(self):
            print "First(): entering"
            super(First, self).__init__()
            print "First(): exiting"
        
        class Second(object):
          def __init__(self):
            print "Second(): entering"
            super(Second, self).__init__()
            print "Second(): exiting"
        
        class Third(First, Second):
          def __init__(self):
            print "Third(): entering"
            super(Third, self).__init__()
            print "Third(): exiting"
        

        According to this article about Method Resolution Order by Guido van Rossum,解析__init__ 的顺序是使用“深度优先从左到右遍历”计算的(在 Python 2.3 之前):

        Third --> First --> object --> Second --> object
        

        删除所有重复项后,除了最后一个,我们得到:

        Third --> First --> Second --> object
        

        所以,让我们看看当我们实例化 Third 类的实例时会发生什么,例如x = Third()

        1. 根据 MRO Third.__init__ 执行。
          • 打印Third(): entering
          • 然后super(Third, self).__init__() 执行,MRO 返回被调用的First.__init__
        2. First.__init__ 执行。
          • 打印First(): entering
          • 然后super(First, self).__init__() 执行,MRO 返回被调用的Second.__init__
        3. Second.__init__ 执行。
          • 打印Second(): entering
          • 然后super(Second, self).__init__() 执行,MRO 返回被调用的object.__init__
        4. object.__init__ 执行(代码中没有打印语句)
        5. 执行回到Second.__init__,然后打印Second(): exiting
        6. 执行回到First.__init__,然后打印First(): exiting
        7. 执行回到Third.__init__,然后打印Third(): exiting

        这详细说明了为什么实例化 Third() 会导致:

        Third(): entering
        First(): entering
        Second(): entering
        Second(): exiting
        First(): exiting
        Third(): exiting
        

        MRO 算法从 Python 2.3 开始改进,可以在复杂的情况下很好地工作,但我想使用“深度优先的从左到右遍历”+“删除重复的期望最后一个”在大多数情况下仍然有效案例(如果不是这种情况,请发表评论)。请务必阅读 Guido 的博文!

        【讨论】:

        • 我还是不明白为什么:First super(First, self).__init__() 的 init 内部调用 Second 的 init,因为这就是 MRO 的规定!
        • @user389955 创建的对象是具有所有 init 方法的 Third 类型。因此,如果您假设 MRO 按特定顺序创建所有 init 函数的列表,每次超级调用时,您都会向前迈出一步,直到到达终点。
        • 我认为第3步需要更多解释:如果Third没有从Second继承,那么super(First, self).__init__会调用object.__init__,返回后会打印“first”。但是因为Third 继承自FirstSecond,而不是在First.__init__ 之后调用object.__init__,MRO 规定只保留对object.__init__ 的最终调用,并且First 中的打印语句和Second 直到 object.__init__ 返回。由于Second 是最后一个调用object.__init__ 的,所以它在Second 内部返回,然后在First 中返回。
        • 有趣的是,PyCharm 似乎知道这一切(它的提示讨论了哪些参数与哪些调用 super 相关。它还具有一些输入协方差的概念,因此它将 List[subclass] 识别为 @987654369 @ if subclasssuperclass 的子类(List 来自 PEP 483 iirc 的 typing 模块)。
        • 不错的帖子,但我错过了有关构造函数参数的信息,即如果 Second 和 First 期望不同的参数会发生什么? First 的构造函数必须处理一些参数并将其余的传递给 Second。那正确吗? First 需要了解 Second 所需的参数,这对我来说听起来不正确。
        【解决方案10】:

        也许还有一些东西可以添加,一个 Django rest_framework 和装饰器的小例子。这为隐含的问题提供了答案:“我为什么要这样做?”

        如前所述:我们使用 Django rest_framework,我们使用通用视图,对于数据库中的每种类型的对象,我们发现自己有一个视图类为对象列表提供 GET 和 POST,以及另一个视图为单个对象提供 GET、PUT 和 DELETE 的类。

        现在我们要用 Django 的 login_required 来装饰 POST、PUT 和 DELETE。请注意这涉及到两个类,但不是两个类中的所有方法。

        一个解决方案可以通过多重继承。

        from django.utils.decorators import method_decorator
        from django.contrib.auth.decorators import login_required
        
        class LoginToPost:
            @method_decorator(login_required)
            def post(self, arg, *args, **kwargs):
                super().post(arg, *args, **kwargs)
        

        其他方法也是如此。

        在我的具体类的继承列表中,我将在ListCreateAPIView 之前添加我的LoginToPostLoginToPutOrDeleteRetrieveUpdateDestroyAPIView 之前。我的具体类的get 将保持未装饰。

        【讨论】:

          【解决方案11】:

          在 learningpythonthehardway 中,我学到了一些叫做 super() 的东西,如果没记错的话,它是一个内置函数。调用 super() 函数可以帮助继承通过父和“兄弟”,并帮助您看得更清楚。我仍然是初学者,但我喜欢分享我在 python2.7 中使用这个 super() 的经验。

          如果您阅读了本页中的 cmets,您会听说 Method Resolution Order (MRO),该方法是您编写的函数,MRO 将使用 Depth-First-Left-to-Right 方案进行搜索和跑步。您可以对此进行更多研究。

          通过添加 super() 函数

          super(First, self).__init__() #example for class First.
          

          您可以使用 super() 连接多个实例和“家庭”,方法是添加其中的每个人。它会执行这些方法,检查它们并确保你没有错过!但是,在之前或之后添加它们确实会有所不同,如果您完成了 learningpythonthehardway 练习 44,您就会知道。让乐趣开始吧!

          以下面的例子,你可以复制粘贴并尝试运行它:

          class First(object):
              def __init__(self):
          
                  print("first")
          
          class Second(First):
              def __init__(self):
                  print("second (before)")
                  super(Second, self).__init__()
                  print("second (after)")
          
          class Third(First):
              def __init__(self):
                  print("third (before)")
                  super(Third, self).__init__()
                  print("third (after)")
          
          
          class Fourth(First):
              def __init__(self):
                  print("fourth (before)")
                  super(Fourth, self).__init__()
                  print("fourth (after)")
          
          
          class Fifth(Second, Third, Fourth):
              def __init__(self):
                  print("fifth (before)")
                  super(Fifth, self).__init__()
                  print("fifth (after)")
          
          Fifth()
          

          它是如何运行的? Fifth() 的实例将是这样的。每一步都是从一个类到另一个添加了超级函数的类。

          1.) print("fifth (before)")
          2.) super()>[Second, Third, Fourth] (Left to right)
          3.) print("second (before)")
          4.) super()> First (First is the Parent which inherit from object)
          

          找到了父级,它会继续到第三和第四!!

          5.) print("third (before)")
          6.) super()> First (Parent class)
          7.) print ("Fourth (before)")
          8.) super()> First (Parent class)
          

          现在所有带有 super() 的类都被访问了!父类已被找到并执行,现在它继续在继承中拆箱函数以完成代码。

          9.) print("first") (Parent)
          10.) print ("Fourth (after)") (Class Fourth un-box)
          11.) print("third (after)") (Class Third un-box)
          12.) print("second (after)") (Class Second un-box)
          13.) print("fifth (after)") (Class Fifth un-box)
          14.) Fifth() executed
          

          上述程序的结果:

          fifth (before)
          second (before
          third (before)
          fourth (before)
          first
          fourth (after)
          third (after)
          second (after)
          fifth (after)
          

          对我来说,通过添加 super() 可以让我更清楚地了解 python 将如何执行我的编码并确保继承可以访问我想要的方法。

          【讨论】:

          • 感谢详细的演示!
          【解决方案12】:

          我想在顶部添加what @Visionscaper says

          Third --> First --> object --> Second --> object
          

          在这种情况下,解释器不会过滤掉对象类,因为它是重复的,而是因为 Second 出现在头位置而不出现在层次子集中的尾部位置。而object只出现在尾部位置,在C3算法中不认为是强位置来确定优先级。

          C 类 L(C) 的线性化 (mro) 是

          • C 类
          • 加上合并
            • 其父 P1、P2、.. = L(P1, P2, ...) 的线性化和
            • 其父 P1、P2、.. 的列表

          线性合并是通过选择显示为列表头部而不是尾部的公共类来完成的,因为顺序很重要(将在下面变得清楚)

          Third 的线性化可以如下计算:

              L(O)  := [O]  // the linearization(mro) of O(object), because O has no parents
          
              L(First)  :=  [First] + merge(L(O), [O])
                         =  [First] + merge([O], [O])
                         =  [First, O]
          
              // Similarly, 
              L(Second)  := [Second, O]
          
              L(Third)   := [Third] + merge(L(First), L(Second), [First, Second])
                          = [Third] + merge([First, O], [Second, O], [First, Second])
          // class First is a good candidate for the first merge step, because it only appears as the head of the first and last lists
          // class O is not a good candidate for the next merge step, because it also appears in the tails of list 1 and 2, 
                          = [Third, First] + merge([O], [Second, O], [Second])
          // class Second is a good candidate for the second merge step, because it appears as the head of the list 2 and 3
                          = [Third, First, Second] + merge([O], [O])            
                          = [Third, First, Second, O]
          

          因此对于以下代码中的 super() 实现:

          class First(object):
            def __init__(self):
              super(First, self).__init__()
              print "first"
          
          class Second(object):
            def __init__(self):
              super(Second, self).__init__()
              print "second"
          
          class Third(First, Second):
            def __init__(self):
              super(Third, self).__init__()
              print "that's it"
          

          这个方法如何解决就很明显了

          Third.__init__() ---> First.__init__() ---> Second.__init__() ---> 
          Object.__init__() ---> returns ---> Second.__init__() -
          prints "second" - returns ---> First.__init__() -
          prints "first" - returns ---> Third.__init__() - prints "that's it"
          

          【讨论】:

          • "而是因为 Second 出现在头部位置,而不出现在层次结构子集中的尾部位置。"不清楚什么是头部或尾部位置,也不知道层次子集是什么,或者您指的是哪个子集。
          • 尾部位置是指在类层次结构中较高的类,反之亦然。基类“对象”位于尾部的末尾。理解 mro 算法的关键是“第二”如何作为“第一”的超级出现。我们通常会假设它是“对象”类。那是真的,但是,仅从“头等舱”的角度来看。但是,从“第三类”的角度来看,“第一类”的层次顺序是不同的,计算方法如上所示。 mro 算法尝试为所有多个继承的类创建这个透视图(或层次子集)
          【解决方案13】:

          您的代码和其他答案都是错误的。他们缺少协作子类工作所需的前两个类中的 super() 调用。

          这里是固定版本的代码:

          class First(object):
              def __init__(self):
                  super(First, self).__init__()
                  print("first")
          
          class Second(object):
              def __init__(self):
                  super(Second, self).__init__()
                  print("second")
          
          class Third(First, Second):
              def __init__(self):
                  super(Third, self).__init__()
                  print("third")
          

          super() 调用在每一步都在 MRO 中找到下一个方法,这就是为什么 First 和 Second 也必须拥有它的原因,否则执行会在 Second.__init__() 的末尾停止。

          这是我得到的:

          >>> Third()
          second
          first
          third
          

          【讨论】:

          • 如果这些类需要不同的参数来初始化自己怎么办?
          • “合作子类化”
          • 这样,两个基类的 init 方法都会被执行,而原始示例只调用 MRO 中遇到的第一个 init。我猜这是“合作子类化”一词所暗示的,但澄清一下会很有用('显式比隐式更好',你知道;))
          • 是的,如果您将不同的参数传递给通过 super 调用的方法,则该方法的所有实现都需要具有兼容的签名。这可以通过关键字参数来实现:接受比方法使用更多的参数,并忽略多余的参数。通常认为这样做很难看,并且在大多数情况下添加新方法更好,但是 init 作为特殊方法名称(几乎?)是唯一的,但具有用户定义的参数。
          • 多重继承的设计在python中真的很糟糕。基类几乎需要知道谁将派生它,派生的还有多少其他基类,以及以什么顺序......否则super要么无法运行(因为参数不匹配),或者它不会调用几个碱基(因为你没有在断开链接的碱基之一中写super)!
          【解决方案14】:

          我知道这并不能直接回答super() 的问题,但我觉得它足够相关,可以分享。

          还有一种方法可以直接调用每个继承的类:

          class First(object): def __init__(self): print '1' class Second(object): def __init__(self): print '2' class Third(First, Second): def __init__(self): Second.__init__(self)

          请注意,如果您这样做,您将不得不手动调用每个,因为我很确定 First__init__() 不会被调用。

          【讨论】:

          • 它不会被调用,因为你没有调用每个继承的类。问题在于,如果FirstSecond 都继承了另一个类并直接调用它,那么这个公共类(菱形的起点)会被调用两次。 super 正在避免这种情况。
          • @Trilarion 是的,我相信它不会。然而,我并不确定,我不想说我知道,即使这不太可能。这是关于object 被调用两次的好点。我没想到。我只是想说明您直接调用父类。
          • 不幸的是,如果 init 尝试访问任何私有方法,则会中断:(
          【解决方案15】:
          class First(object):
            def __init__(self, a):
              print "first", a
              super(First, self).__init__(20)
          
          class Second(object):
            def __init__(self, a):
              print "second", a
              super(Second, self).__init__()
          
          class Third(First, Second):
            def __init__(self):
              super(Third, self).__init__(10)
              print "that's it"
          
          t = Third()
          

          输出是

          first 10
          second 20
          that's it
          

          调用Third() 定位到Third 中定义的init。并在该例程中调用 super 调用 First 中定义的 init。 MRO=[第一,第二]。 现在调用 First 中定义的 init 中的 super 将继续搜索 MRO 并找到 Second 中定义的 init,任何对 super 的调用都会命中默认对象 init强>。我希望这个例子能阐明这个概念。

          如果您不从 First 调用 super。链条停止,您将获得以下输出。

          first 10
          that's it
          

          【讨论】:

          • 那是因为在 First 类中,你先调用了 'print' 然后调用了 'super'。
          • 那是为了说明调用顺序
          【解决方案16】:

          这就是我如何解决具有用于初始化的不同变量的多重继承以及具有具有相同函数调用的多个 MixIn 的问题。我必须显式地将变量添加到传递的 **kwargs 并添加一个 MixIn 接口作为超级调用的端点。

          这里的A 是一个可扩展的基类,BC 都是提供函数f 的 MixIn 类。 AB 在它们的 __init__C 中都期望参数 vC 期望 w。 函数f 接受一个参数yQ 继承自所有三个类。 MixInFBC 的 mixin 接口。

          
          class A(object):
              def __init__(self, v, *args, **kwargs):
                  print "A:init:v[{0}]".format(v)
                  kwargs['v']=v
                  super(A, self).__init__(*args, **kwargs)
                  self.v = v
          
          
          class MixInF(object):
              def __init__(self, *args, **kwargs):
                  print "IObject:init"
              def f(self, y):
                  print "IObject:y[{0}]".format(y)
          
          
          class B(MixInF):
              def __init__(self, v, *args, **kwargs):
                  print "B:init:v[{0}]".format(v)
                  kwargs['v']=v
                  super(B, self).__init__(*args, **kwargs)
                  self.v = v
              def f(self, y):
                  print "B:f:v[{0}]:y[{1}]".format(self.v, y)
                  super(B, self).f(y)
          
          
          class C(MixInF):
              def __init__(self, w, *args, **kwargs):
                  print "C:init:w[{0}]".format(w)
                  kwargs['w']=w
                  super(C, self).__init__(*args, **kwargs)
                  self.w = w
              def f(self, y):
                  print "C:f:w[{0}]:y[{1}]".format(self.w, y)
                  super(C, self).f(y)
          
          
          class Q(C,B,A):
              def __init__(self, v, w):
                  super(Q, self).__init__(v=v, w=w)
              def f(self, y):
                  print "Q:f:y[{0}]".format(y)
                  super(Q, self).f(y)
          

          【讨论】:

          • 我认为这也许应该是一个单独的问答,因为 MRO 本身就是一个足够大的话题,而无需处理具有继承的函数之间的不同参数(多重继承是一种特殊的这种情况)。
          • 理论上是的。实际上,每次在python中遇到Diamond继承时都会出现这种情况,所以我在这里添加了它。因为,这是我每次无法完全避免钻石继承的地方。以下是未来我的一些额外链接:rhettinger.wordpress.com/2011/05/26/super-considered-supercode.activestate.com/recipes/…
          • 我们想要的是具有语义上有意义的参数名称的程序。但是在这个例子中,几乎所有的参数都是匿名命名的,这使得原来的程序员记录代码和另一个程序员阅读代码变得更加困难。
          • 向 github repo 提出具有描述性名称的拉取请求将不胜感激
          • @brent.payne 我认为@Arthur 的意思是你的整个方法依赖于使用args / kwargs 而不是命名参数。
          【解决方案17】:

          另一个尚未涉及的问题是为类的初始化传递参数。由于super 的目的地取决于子类,因此传递参数的唯一好方法是将它们打包在一起。然后注意不要有不同含义的相同参数名称。

          例子:

          class A(object):
              def __init__(self, **kwargs):
                  print('A.__init__')
                  super().__init__()
          
          class B(A):
              def __init__(self, **kwargs):
                  print('B.__init__ {}'.format(kwargs['x']))
                  super().__init__(**kwargs)
          
          
          class C(A):
              def __init__(self, **kwargs):
                  print('C.__init__ with {}, {}'.format(kwargs['a'], kwargs['b']))
                  super().__init__(**kwargs)
          
          
          class D(B, C): # MRO=D, B, C, A
              def __init__(self):
                  print('D.__init__')
                  super().__init__(a=1, b=2, x=3)
          
          print(D.mro())
          D()
          

          给予:

          [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
          D.__init__
          B.__init__ 3
          C.__init__ with 1, 2
          A.__init__
          

          直接调用超类__init__ 以更直接地分配参数很诱人,但如果超类中有任何super 调用和/或更改MRO 并且可能多次调用A 类,则失败,具体取决于关于实施。

          总结:协作继承和用于初始化的超级参数和特定参数不能很好地协同工作。

          【讨论】:

            【解决方案18】:

            这被称为Diamond Problem,该页面在Python上有一个条目,但简而言之,Python会从左到右调用超类的方法。

            【讨论】:

            • 这不是钻石问题。钻石问题涉及四个类,而 OP 的问题只涉及三个。
            • 这根本不是真正的钻石问题,因为没有传递共享基类(除了object,但这是所有类的通用基类并且不this 问题中的作用)。 Python 调用方法的确切顺序那么简单,C3 linearisation of the class hierarchy 可能导致非常不同的顺序。
            猜你喜欢
            相关资源
            最近更新 更多