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 是 Amphibian
、LandVehicle
、WaterCraft
、Vehicle
,最后是 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 可能会以 WaterCraft
和 LandVehcle
之间的其他基数 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 代码。
如以下示例所示,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 是 Amphibian
、LandVehicle
、WaterCraft
、Vehicle
,最后是 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 可能会以 WaterCraft
和 LandVehcle
之间的其他基数 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 代码。