【问题标题】:super() strange behavior in diamond inheritance in pythonpython中钻石继承中的super()奇怪行为
【发布时间】:2018-09-26 21:47:30
【问题描述】:

如以下示例所示,super() 在用于菱形继承时有一些奇怪的(至少对我而言)行为。

class Vehicle:
    def start(self):
        print("engine has been started")

class LandVehicle(Vehicle):
    def start(self):
        super().start()
        print("tires are safe to use")

class WaterCraft(Vehicle):
    def start(self):
        super().start()
        print("anchor has been pulled up")

class Amphibian(LandVehicle, WaterCraft):
    def start(self):
        # we do not want to call WaterCraft.start, amphibious
        # vehicles don't have anchors
        LandVehicle.start(self)
        print("amphibian is ready for travelling on land")

amphibian = Amphibian()
amphibian.start()

以上代码产生以下输出:

engine has been started
anchor has been pulled up
tires are safe to use
amphibian is ready for travelling on land

当我调用super().some_method() 时,我绝不会期望调用同一继承级别的类的方法。因此,在我的示例中,我不希望 anchor has been pulled up 出现在输出中。

调用super() 的类甚至可能不知道最终调用其方法的另一个类。在我的示例中,LandVehicle 甚至可能不知道 WaterCraft

这种行为是否正常/预期?如果是,其背后的基本原理是什么?

【问题讨论】:

  • 这种行为是 Python 的super 的重点。 (是的,这个名字很混乱。)
  • 所以我不应该使用super 来调用基类方法?那有什么好处呢,为什么还能顺着继承链下去呢?
  • This 在技术上是重复的,但我既不喜欢问题也不喜欢答案。我认为我们可以做得更好。
  • 如果Amphibian 没有锚,那么它不应该从WaterCraft 继承,或者WaterCraft 应该允许可选 锚。

标签: python python-3.x inheritance super diamond-problem


【解决方案1】:

当您在 Python 中使用继承时,每个类都定义了一个方法解析顺序 (MRO),用于决定查找类属性时的查找位置。例如,对于您的 Amphibian 课程,MRO 是 AmphibianLandVehicleWaterCraftVehicle,最后是 object。 (您可以致电Amphibian.mro() 亲自查看。)

MRO 如何派生的具体细节有点复杂(如果您有兴趣,可以找到它的工作原理说明)。重要的是要知道任何子类总是列在其父类之前,如果进行多重继承,则子类的所有父类将按照它们在class 语句中的相同相对顺序(其他类可能出现在父母之间,但它们永远不会相对于彼此颠倒)。

当您使用super 调用一个被覆盖的方法时,它看起来虽然MRO 就像它对任何属性查找所做的那样,但它开始搜索的时间比平时更远。具体来说,它开始搜索“当前”类之后的属性。我所说的“当前”是指包含调用super 的方法的类(即使调用该方法的对象属于其他一些更派生的类)。因此,当LandVehicle.__init__ 调用super().__init__ 时,它开始在MRO 中LandVehicle 之后的第一个类中检查__init__ 方法,并找到WaterCraft.__init__

这建议了一种解决问题的方法。您可以将 Amphibian 名称 WaterCraft 作为其第一个基类,并将 LandVehicle 第二个基类:

class Amphibian(Watercraft, LandVehicle):
    ...

更改基地的顺序也会改变它们在 MRO 中的顺序。当Amphibian.__init__ 直接按名称调用LandVehicle.__init__(而不是使用super)时,随后的super 调用将跳过WaterCraft,因为调用它们的类已经在MRO 中更进一步。因此,其余的 super 调用将按您的预期工作。

但这并不是一个很好的解决方案。当你显式地命名一个这样的基类时,如果你有更多的子类想要以不同的方式做事,你可能会发现它会破坏事情。例如,从上面重新排序的基类Amphibian 派生的类可能会在WaterCraftLandVehcle 之间得到其他基类,当Amphibian.__init__ 调用@987654351 时,它们的__init__ 方法也会意外跳过直接@。

更好的解决方案是允许依次调用所有 __init__ 方法,但要考虑到它们的部分,您可能不希望总是遇到可以单独覆盖的其他方法。

例如,您可以将WaterCraft 更改为:

class WaterCraft(Vehicle):
    def start(self):
        super().start()
        self.weigh_anchor()

    def weigh_anchor(self):
        print("anchor has been pulled up")

Amphibian 类可以覆盖锚的特定行为(例如,什么都不做):

class Amphibian(LandVehicle, WaterCraft):
    def start(self):
        super().start(self)
        print("amphibian is ready for travelling on land")

    def weigh_anchor(self):
        pass # no anchor to weigh, so do nothing

当然,在这种特定情况下,WaterCraft 除了提升它的锚点之外什么都不做,删除WaterCraft 作为Amphibian 的基类会更简单。但同样的想法通常适用于非平凡的代码。

【讨论】:

  • 好的,感谢您的详细解答。我真的很喜欢应该排除该方法的解决方案。我唯一剩下的担心是您无法访问基类,因此无法对其应用此类更改。 (在这种情况下,其他解决方案可能仍然有效,但如果它也不能使用怎么办......)
猜你喜欢
  • 2020-10-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-16
相关资源
最近更新 更多