Python 多重继承中的 self 和 super

Python self and super in multiple inheritance

在 Raymond Hettinger 在 PyCon 2015 的演讲“Super considered super speak”中,他解释了在多重继承上下文中使用 super in Python 的优势。这是 Raymond 在演讲中使用的例子之一:

class DoughFactory(object):
    def get_dough(self):
        return 'insecticide treated wheat dough'


class Pizza(DoughFactory):
    def order_pizza(self, *toppings):
        print('Getting dough')
        dough = super().get_dough()
        print('Making pie with %s' % dough)
        for topping in toppings:
            print('Adding: %s' % topping)


class OrganicDoughFactory(DoughFactory):
    def get_dough(self):
        return 'pure untreated wheat dough'


class OrganicPizza(Pizza, OrganicDoughFactory):
    pass


if __name__ == '__main__':
    OrganicPizza().order_pizza('Sausage', 'Mushroom')

听众中有人 asked Raymond 关于使用 self.get_dough() 而不是 super().get_dough() 的区别。我不太理解 Raymond 的简短回答,但我对这个示例的两个实现进行了编码以查看差异。两种情况的输出相同:

Getting dough
Making pie with pure untreated wheat dough
Adding: Sausage
Adding: Mushroom

如果您使用 self.get_dough() 将 class 顺序从 OrganicPizza(Pizza, OrganicDoughFactory) 更改为 OrganicPizza(OrganicDoughFactory, Pizza),您将得到以下结果:

Making pie with pure untreated wheat dough

但是如果你使用 super().get_dough() 这是输出:

Making pie with insecticide treated wheat dough

我理解 Raymond 解释的 super() 行为。但是 self 在多重继承场景中的预期行为是什么?

澄清一下,有四种情况,基于更改 Pizza.order_pizza 中的第二行和 OrganicPizza 的定义:

  1. super(), (Pizza, OrganicDoughFactory) (原版): 'Making pie with pure untreated wheat dough'
  2. self, (Pizza, OrganicDoughFactory): 'Making pie with pure untreated wheat dough'
  3. super(), (OrganicDoughFactory, Pizza): 'Making pie with insecticide treated wheat dough'
  4. self, (OrganicDoughFactory, Pizza): 'Making pie with pure untreated wheat dough'

案例 3. 是让您感到惊讶的案例;如果我们切换继承顺序但仍然使用 super,我们显然最终会调用原始的 DoughFactory.get_dough.


super真正做的是问"which is next in the MRO (method resolution order)?"那么OrganicPizza.mro()是什么样的呢?

  • (Pizza, OrganicDoughFactory): [<class '__main__.OrganicPizza'>, <class '__main__.Pizza'>, <class '__main__.OrganicDoughFactory'>, <class '__main__.DoughFactory'>, <class 'object'>]
  • (OrganicDoughFactory, Pizza): [<class '__main__.OrganicPizza'>, <class '__main__.OrganicDoughFactory'>, <class '__main__.Pizza'>, <class '__main__.DoughFactory'>, <class 'object'>]

这里的关键问题是:Pizza之后是哪个?当我们从 Pizza 内部调用 super 时,这就是 Python 将去寻找 get_dough* 的地方。对于 1. 和 2. 它是 OrganicDoughFactory,所以我们得到纯净的、未经处理的面团,但对于 3. 和 4. 它是原始的、经过杀虫剂处理的 DoughFactory.


那为什么 self 不一样呢? self 始终是 实例 ,因此 Python 从 MRO 开始就开始寻找 get_dough。在这两种情况下,如上所示,OrganicDoughFactory 在列表中比 DoughFactory 更早,这就是为什么 self 版本 总是 得到未经处理的面团; self.get_dough 总是解析为 OrganicDoughFactory.get_dough(self)


* 我认为这在 Python 2.x 中使用的 super 的双参数形式实际上更清楚,即 super(Pizza, self).get_dough();第一个参数是要跳过的 class(即 Python 在 class 之后查看 MRO 的其余部分)。

我想就此分享一些看法。

如果覆盖父 class 的 get_dough() 方法,则可能无法调用 self.get_dough(),如下所示:

class AbdullahStore(DoughFactory):
    def get_dough(self):
        return 'Abdullah`s special ' + super().get_dough()

我认为这是实践中经常出现的场景。如果我们直接调用 DoughFactory.get_dough(self) 那么行为是固定的。 class 派生 AbdullahStore 必须覆盖 完整的方法,不能重复使用 AbdullahStore 的 'added value'。另一方面,如果我们使用 super.get_dough(self),这具有模板的风格: 在从 AbdullahStore 派生的任何 class 中,比如说

class Kebab(AbdullahStore):
    def order_kebab(self, sauce):
        dough = self.get_dough()
        print('Making kebab with %s and %s sauce' % (dough, sauce))

我们可以 'instantiate' get_dough()AbdullahStore 中使用不同的方式,通过像这样在 MRO 中拦截它

class OrganicKebab(Kebab, OrganicDoughFactory):pass

这是它的作用:

Kebab().order_kebab('spicy')
Making kebab with Abdullah`s special insecticide treated wheat dough and spicy sauce
OrganicKebab().order_kebab('spicy')
Making kebab with Abdullah`s special pure untreated wheat dough and spicy sauce

由于 OrganicDoughFactory 有一个单亲 DoughFactory,我保证它会在 DoughFactory 之前插入 MRO,从而覆盖所有前面的方法 class在 MRO 中。我花了一些时间来理解用于构建 MRO 的 C3 线性化算法。 问题是两条规则

children come before parents
parents order is preserved

根据此参考 https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ 尚未明确定义顺序。在 class 层次结构中

D->C->B->A
 \      /
   --E--

(class A; class B(A); class C(B); class E(A); class D( C,E)) E 将插入 MRO 中的哪个位置?是 DCBEA 还是 DCEBA?也许在一个人可以自信地回答这样的问题之前,开始到处插入 super 并不是一个好主意。我仍然不完全确定,但我认为 明确的 C3 线性化并且将在本例中选择排序 DCBEA,确实 允许我们按照我们的方式来做拦截技巧,毫不含糊。

现在,我想你可以预测

的结果
class KebabNPizza(Kebab, OrganicPizza): pass
KebabNPizza().order_kebab('hot')

这是改良的烤肉串:

Making kebab with Abdullah`s special pure untreated wheat dough and hot sauce

但您可能花了一些时间来计算。

当我第一次看 super 文档 https://docs.python.org/3.5/library/functions.html?highlight=super#super 之前,我来自 C++ 背景,就像 "wow, ok here are the rules, but how this can ever work and not stub you in the back?"。 现在我对它有了更多的了解,但仍然不愿意到处插入 super 。我认为我看到的大多数代码库都是这样做的,只是因为 super() 比基础 class 名称更便于键入。 这甚至不是在谈论 super() 在链接 __init__ 函数时的极端使用。我在实践中观察到的是,每个人都使用方便 class(而不是通用的)的签名编写构造函数,并使用 super() 调用他们认为是他们的基础 class构造函数。