MRO,super 在 python 中的工作原理

How MRO, super works in python

有人可以帮助我了解 MRO 在 python 中的工作原理吗? 假设我有四个 classes - Character、Thief、Agile、Sneaky。 Character是Thief的超级class,Agile和Sneaky是兄妹。请在下面查看我的代码和问题

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 class(通过 super),同样的事情会发生 -所有参数都被解包,与 Sneaky 参数交叉引用(这是在设置 sneaky = False 时),然后打包在 kwargs 中并发送给 Character。然后 Character inint 方法中的所有内容都将 运行 并且所有值都将被设置(如名称 = "Parker")。

我认为 MRO 在回归的过程中如何运作

既然所有内容都已进入角色 class 并且角色初始化方法中的所有内容都有 运行,现在它必须回到敏捷和偷偷摸摸的 classes 并且完成 运行 在他们的 init 方法中完成所有事情(或者在他们的 super 下的所有事情)。因此,它将首先返回到 Sneaky class 并完成它的 init 方法,然后返回到 Agile class 并完成其剩余的 init 方法(分别)。

我有没有混淆它?呸。抱歉,我知道很多,但我真的被困在这里,我正试图清楚地了解 MRO 的工作原理。

谢谢大家

您发布的代码甚至无法编译,更不用说 运行。但是,猜测它应该如何工作……

是的,你基本上是对的。

但是您应该可以通过两种方式自己验证这一点。而知道如何验证可能比知道答案更重要。


首先,打印出Thief.mro()。它应该看起来像这样:

[Thief, Agile, Sneaky, Character, object]

然后你可以看到哪些 classes 提供了一个 __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 调用事物的顺序,你是对的,而堆栈在返回途中弹出的顺序显然恰恰相反。


但是,与此同时,您有一个细节错误:

sent to the Sneaky class (via super), where the same thing will happen - all arguments get unpacked, cross-referenced with the Sneaky parameters (this is when sneaky = False is set)

这是设置 parameter/local 变量 sneaky 的地方,但是 self.sneaky 直到 [=17] 之后才设置=] returns。在那之前(包括在 Character.__init__ 期间,对于你选择在 Sneaky 之后放入的任何其他 mixins 也是类似的),在 self.__dict__ 中没有 sneaky,所以如果有人尝试查找 self.sneaky,他们只能找到 class 属性——它的值是错误的。


这又引出了另一点:那些 class 属性有什么用?如果您希望它们提供默认值,那么您已经在初始化参数上获得了默认值,因此它们是无用的。

如果您希望它们在初始化期间提供值,那么它们可能是错误的,所以它们比无用还糟糕。如果在调用 Character.__init__ 之前需要有一个 self.sneaky,方法很简单:只需将 self.sneaky = sneaky 向上移动 之前 [=33] =]打电话。

事实上,这是 Python 的 "explicit super" 模型的优势之一。在一些语言中,比如 C++,构造函数总是被自动调用,无论是从内到外还是从外到内。Python 强迫你明确地做它不太方便,也更难出错——但这意味着你可以选择做您在基础 class 获得机会之前或之后的设置(当然,每个机会都有一点),这有时很有用。