有人可以帮助我了解MRO在python中的工作原理吗? 假设我有四个类 - 角色,小偷,敏捷,鬼鬼祟祟。角色是盗贼的超级职业,敏捷和鬼鬼祟祟是兄弟姐妹。请参阅下面的代码和问题
class Character:
def __init__(self, name="", **kwargs):
if not name:
raise ValueError("'name' is required")
self.name = name
for key, value in kwargs.items():
setattr(self, key, value)
class Agile:
agile = True
def __init__(self, agile=True, *args, **kwargs):
super().__init__(*args, **kwargs)
self.agile = agile
class Sneaky:
sneaky = True
def __init__(self, sneaky=True, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sneaky = sneaky
class Thief(Agile, Sneaky, Character):
def pickpocket(self):
return self.sneaky and bool(random.randint(0, 1))
parker = Thief(name="Parker", sneaky=False)
所以,这就是我认为正在发生的事情,如果我理解正确,请告诉我。
由于敏捷在列表中排在第一位,因此所有参数首先发送到敏捷,其中参数将与敏捷参数交叉引用。如果存在匹配值将被分配,那么所有没有匹配关键字的内容都将打包在 *kwargs 中并发送到 Sneaky 类(通过 super(,在那里会发生同样的事情 - 所有参数都被解压缩,与 Sneaky 参数交叉引用(这是设置 sneaky = False 时(,然后打包在 kwargs 中并发送到 Character。然后,Character inint 方法中的所有内容都将运行,并且将设置所有值(如名称 = "Parker"(。
我认为 MRO 在回来的路上是如何工作的
现在一切都进入了 Character 类,并且 Character init 方法中的所有内容都已经运行,现在它必须回到敏捷和 Sneaky 类并完成运行其 init 方法中的所有内容(或其 super 下的所有内容(。因此,它将首先返回到 Sneaky 类并完成其 init 方法,然后返回到敏捷类并完成其 init 方法的其余部分(分别(。
我在任何地方都感到困惑吗?唷。对不起,我知道这很多,但我真的被困在这里,我试图清楚地了解 MRO 的工作原理。
谢谢大家。
你发布的代码甚至不能编译,更不用说运行了。但是,猜测它应该如何工作...
是的,你基本上做对了。
但是您应该能够通过两种方式自己验证这一点。知道如何验证它可能比知道答案更重要。
首先,只需打印出Thief.mro()
。它应该看起来像这样:
[Thief, Agile, Sneaky, Character, object]
然后你可以看到哪些类提供了一个__init__
方法,因此如果每个人都只调用super
,它们将如何链接起来:
>>> [cls for cls in Thief.mro() if '__init__' in cls.__dict__]
[Agile, Sneaky, Character, object]
而且,为了确保Agile
确实首先被调用:
>>> Thief.__init__
<function Agile.__init__>
其次,可以在调试器中运行代码并单步执行调用。
或者,您可以在每个语句的顶部和底部添加print
语句,如下所示:
def __init__(self, agile=True, *args, **kwargs):
print(f'>Agile.__init__(agile={agile}, args={args}, kwargs={kwargs})')
super().__init__(*args, **kwargs)
self.agile = agile
print(f'<Agile.__init__: agile={agile}')
(你甚至可以编写一个装饰器来自动执行此操作,并带有一点inspect
魔法。
如果你这样做,它会打印出类似以下内容:
> Agile.__init__(agile=True, args=(), kwargs={'name': 'Parker', 'sneaky':False})
> Sneaky.__init__(sneaky=False, args=(), kwargs={'name': 'Parker'})
> Character.__init__(name='Parker', args=(), kwargs={})
< Character.__init__: name: 'Parker'
< Sneaky.__init__: sneaky: False
< Agile.__init__: agile: True
所以,你对事物通过super
调用的顺序是正确的,而堆栈在回来的路上被弹出的顺序显然恰恰相反。
但是,与此同时,你有一个细节是错误的:
发送到 Sneaky 类(通过 super(,在那里会发生同样的事情 - 所有参数都被解压缩,与 Sneaky 参数交叉引用(这是设置 sneaky = False 时(
这是设置参数/局部变量sneaky
的地方,但self.sneaky
直到super
返回后才会设置。在那之前(包括Character.__init__
期间,以及你选择在Sneaky
之后投入的任何其他mixin(,self.__dict__
中没有sneaky
,所以如果有人试图查找self.sneaky
,他们只能找到具有错误值的类属性。
这就提出了另一点:这些类属性是干什么用的?如果您希望它们提供默认值,则初始值设定项参数上已经具有默认值,因此它们毫无用处。
如果您希望它们在初始化期间提供值,那么它们可能是错误的,因此它们比无用更糟糕。如果您需要在呼叫Character.__init__
之前进行self.sneaky
,方法很简单:只需在super()
呼叫之前向上移动self.sneaky = sneaky
即可。
事实上,这是Python的"显式super
"模型的优势之一。在某些语言(如 C++(中,构造函数总是自动调用的,无论是从内到外还是从外到内。Python 强迫你显式地这样做不太方便,也更难出错——但这意味着你可以选择在基类获得机会之前或之后进行设置(或者,当然,每个基类的一点(,这有时很有用。