python 钻石继承中的 super() 奇怪行为

super() strange behavior in diamond inheritance in python

如以下示例所示,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() 时,我绝不会期望调用同一继承级别上的 class 的方法。所以在我的示例中,我不希望 anchor has been pulled up 出现在输出中。

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

这种行为是否 normal/expected 如果是,其背后的基本原理是什么?

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

MRO 是如何派生的确切细节有点复杂(但如果您有兴趣,可以找到有关其工作原理的描述)。重要的是要知道任何 child class 总是在它的 parent class 之前列出,如果正在进行多重继承,所有 parent child class 的 s 将与它们在 class 语句中的相对顺序相同(其他 classes 可能出现在 parents 之间,但它们永远不会相互颠倒)。

当您使用 super 调用重写的方法时,它看起来虽然 MRO 与它对任何属性查找所做的一样,但它比平时更深入地开始搜索。具体来说,它会在 "current" class 之后开始搜索属性。 "current" 我的意思是,class 包含调用 super 的方法(即使调用该方法的 object 是其他派生的 class).所以当 LandVehicle.__init__ 调用 super().__init__ 时,它开始检查 MRO 中 LandVehicle 之后第一个 class 中的 __init__ 方法,并找到 WaterCraft.__init__.

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

class Amphibian(Watercraft, LandVehicle):
    ...

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

但这并不是一个很好的解决方案。当你像这样明确命名一个基 class 时,如果你有更多 child class 想要以不同的方式做事,你可能会发现它稍后会破坏事情。例如,从上面的 reordered-base Amphibian 派生的 class 可能会以 WaterCraftLandVehcle 之间的其他基数 class 结束,这当 Amphibian.__init__ 直接调用 LandVehcle.__init__ 时,它们的 __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 可以覆盖锚特定行为(例如什么也不做):

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 作为基础 class 会更简单Amphibian。但同样的想法通常适用于 non-trivial 代码。