Li Huanshuai's Blogs

Python MRO

在 Python 中,类的方法解析顺序(Method Resolution Order,MRO)是一个是面向对象编程中的重要却又容易被忽视的概念。MRO 定义了类的方法解析顺序,即当一个类的实例调用一个方法时,Python 解释器会按照 MRO 的顺序查找这个方法。本文将介绍 Python 中的 MRO 算法,以及如何使用 C3 算法来计算 MRO。

MRO

在 Python 中,每个类都有一个 MRO 列表,这个列表定义了类的方法解析顺序。MRO 列表是一个有序的类列表,每个类都有一个唯一的位置。当一个类的实例调用一个方法时,Python 解释器会按照 MRO 列表的顺序查找这个方法。

之所以需要 MRO 列表,是因为 Python 支持多重继承。在多重继承中,一个类可以继承多个父类的方法。为了解决多重继承中的方法冲突问题,Python 需要提供一个方法解析顺序,以确定调用哪个方法。

在一般编程实践中,多重继承是一个容易引起混乱的地方,常常是不被推荐的。但是在某些情况下,多重继承是有用的,比如 mixin 模式。在这种情况下,MRO 列表就变得非常重要。

Python 中的 MRO 使用 C3 算法来计算。C3 算法是一种广度优先搜索算法,它可以保证满足多重继承的方法解析顺序。

C3 算法

C3 算法是一种广度优先搜索算法,它可以保证满足多重继承的方法解析顺序。C3 算法的核心思想是:对于一个类的 MRO 列表,它的父类的 MRO 列表必须在它的 MRO 列表之前。

Python 创始人Guido van Rossum这样总结 C3 算法:“基本上,在 C3 背后的想法是,如果你写下在复杂的类层级中继承关系所施加的所有次序规则,这个算法将确定出满足所有这些规则的这些类的一个单调次序。如果不能确定出这样的次序,这个算法会失败。”

C3 算法的过程可以借助 Wikipedia 上的伪代码来理解。下面是一个简单的例子,展示了 C3 算法的过程:

class O
class A extends O
class B extends O
class C extends O
class D extends O
class E extends O
class K1 extends A, B, C
class K2 extends D, B, E
class K3 extends D, A
class Z extends K1, K2, K3

在这个例子中,我们定义了一个类层级,其中包含了多重继承。用图表示这个类层级如下:

alt text

在这个例子中,我们可以使用 C3 算法来计算类 Z 的 MRO 列表。下面是计算过程:

 L(O)  := [O]                       // O的线性化就是平凡的单例列表[O]因为O没有父类

 L(A)  := [A] + merge(L(O), [O])    // A的线性化是A加上它的父类的线性化与父类列表的归并
        = [A] + merge([O], [O])
        = [A, O]                    // 其结果是简单的将A前置于它的单一父类的线性化

 L(B)  := [B, O]                    // BCD和E的线性化的计算类似于A
 L(C)  := [C, O]
 L(D)  := [D, O]
 L(E)  := [E, O]

 L(K1) := [K1] + merge(L(A), L(B), L(C), [A, B, C])          // 首先找到K1的父类的线性化L(A)L(B)和L(C)接着将它们归并于父类列表[A, B, C]
        = [K1] + merge([A, O], [B, O], [C, O], [A, B, C])    // 类A是第一个归并步骤的良好候选者因为它只出现为第一个和最后一个列表的头部元素
        = [K1, A] + merge([O], [B, O], [C, O], [B, C])       // 类O不是第二个归并步骤的良好候选者因为它还出现在列表2和列表3的尾部中但是类B是良好候选者
        = [K1, A, B] + merge([O], [O], [C, O], [C])          // 类C是良好候选者类O仍出现在列表3的尾部中
        = [K1, A, B, C] + merge([O], [O], [O])               // 最后类O是有效候选者这还竭尽了所有余下的列表
        = [K1, A, B, C, O]

 L(K2) := [K2] + merge(L(D), L(B), L(E), [D, B, E])
        = [K2] + merge([D, O], [B, O], [E, O], [D, B, E])    // 选择D
        = [K2, D] + merge([O], [B, O], [E, O], [B, E])       // 不选O选择B
        = [K2, D, B] + merge([O], [O], [E, O], [E])          // 不选O选择E
        = [K2, D, B, E] + merge([O], [O], [O])               // 选择O
        = [K2, D, B, E, O]

 L(K3) := [K3] + merge(L(D), L(A), [D, A])
        = [K3] + merge([D, O], [A, O], [D, A])               // 选择D
        = [K3, D] + merge([O], [A, O], [A])                  // 不选O选择A
        = [K3, D, A] + merge([O], [O])                       // 选择O
        = [K3, D, A, O]

 L(Z)  := [Z] + merge(L(K1), L(K2), L(K3), [K1, K2, K3])
        = [Z] + merge([K1, A, B, C, O], [K2, D, B, E, O], [K3, D, A, O], [K1, K2, K3])    // 选择K1
        = [Z, K1] + merge([A, B, C, O], [K2, D, B, E, O], [K3, D, A, O], [K2, K3])        // 不选A选择K2
        = [Z, K1, K2] + merge([A, B, C, O], [D, B, E, O], [K3, D, A, O], [K3])            // 不选A不选D选择K3
        = [Z, K1, K2, K3] + merge([A, B, C, O], [D, B, E, O], [D, A, O])                  // 不选A选择D
        = [Z, K1, K2, K3, D] + merge([A, B, C, O], [B, E, O], [A, O])                     // 选择A
        = [Z, K1, K2, K3, D, A] + merge([B, C, O], [B, E, O], [O])                        // 选择B
        = [Z, K1, K2, K3, D, A, B] + merge([C, O], [E, O], [O])                           // 选择C
        = [Z, K1, K2, K3, D, A, B, C] + merge([O], [E, O], [O])                           // 不选O选择E
        = [Z, K1, K2, K3, D, A, B, C, E] + merge([O], [O], [O])                           // 选择O
        = [Z, K1, K2, K3, D, A, B, C, E, O]                                               // 完成

从这个例子看出,C3 算法的过程和求拓扑排序有些类似,本质上两者都是通过广度优先搜索来计算一个有序列表。

总结

MRO 是 Python 中的一个重要概念,它定义了类的方法解析顺序。MRO 使用 C3 算法来计算,C3 算法是一种广度优先搜索算法,它可以保证满足多重继承的方法解析顺序。在编写多重继承的代码时,我们需要了解 MRO 的计算过程,以便正确地继承和调用父类的方法。

参考资料

  1. Python MRO
  2. C3 linearization