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

时间:2018-09-26 21:47:31

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

如以下示例所示,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

此行为是否正常/是否正常?如果是,那么背后的原因是什么?

1 个答案:

答案 0 :(得分:3)

在Python中使用继承时,每个类都定义一个“方法解析顺序”(MRO),该方法用于决定在查找类属性时应在哪里查找。例如,对于您的Amphibian类,MRO为AmphibianLandVehicleWaterCraftVehicle,最后是object。 (您可以通过致电Amphibian.mro()亲自查看。)

有关MRO派生方式的确切细节有些复杂(尽管您可以在感兴趣的地方找到其运作方式的描述)。要知道的重要一点是,任何子类总是在其父类之前列出,并且如果正在进行多重继承,则子类的所有父类将以与class语句中相同的相对顺序排列(其他班级可能会出现在父母之间,但它们永远不会相互颠倒)。

当您使用super调用重写的方法时,它看起来像MRO一样,适用于任何属性查找,但是它比平时更进一步地开始搜索。具体来说,它开始在“当前”类之后搜索属性。我所说的“当前”是指包含super调用方法的类(即使正在调用该方法的对象是其他派生类的另一种)。因此,当LandVehicle.__init__调用super().__init__时,它将开始检查MRO中__init__之后的第一类中的LandVehicle方法,并找到WaterCraft.__init__

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

class Amphibian(Watercraft, LandVehicle):
    ...

更改碱基的顺序也将更改其在MRO中的顺序。当Amphibian.__init__直接通过名称(而不是使用LandVehicle.__init__来调用super时,后续的super调用将跳过WaterCraft,因为它们被称为在MRO中已经从更远的地方开始了。因此,其余super个调用将按预期工作。

但这并不是一个很好的解决方案。当您显式地命名这样的基类时,如果您有更多的子类想要以不同的方式处理事情,则可能会发现以后打乱了事情。例如,从上面的重新排序基数Amphibian派生的类可能最终在WaterCraftLandVehcle之间的其他基类中,也将跳过其__init__方法Amphibian.__init__直接致电LandVehcle.__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的基类甚至更简单。但是,同样的想法通常也适用于非平凡的代码。