注意: 虽然 JavaScript 对于本网站并非必不可少,但您与内容的互动将受到限制。请启用 JavaScript 以获得完整体验。

Python 2.3 方法解析顺序

作者:Michele Simionato

摘要本文档适用于希望了解 Python 2.3 中使用的 C3 方法解析顺序的 Python 程序员。 虽然本文档并非面向新手,但它具有很强的教学性,并包含许多已解决的示例。我没有找到其他公开可用的具有相同范围的文档,因此本文档应该很有用。

免责声明

我将本文档捐赠给 Python 软件基金会,遵循 Python 2.3 许可证。 与往常一样,我提醒读者,以下内容应该是正确的,但我不做任何保证。 使用本文档的风险自负!

鸣谢

感谢 Python 邮件列表中所有给我支持的人们。感谢 Paul Foley 指出了各种不准确之处,并促使我添加了关于本地优先级排序的部分。感谢 David Goodger 在 reStructuredText 中提供的格式化帮助。感谢 David Mertz 在编辑方面的帮助。感谢 Joan G. Stark 绘制的 pythonic 图片。最后,感谢 Guido van Rossum 热心地将本文档添加到官方 Python 2.3 主页。

                      .-=-.          .--.
          __        .'     '.       /  " )
  _     .'  '.     /   .-.   \     /  .-'\
 ( \   / .-.  \   /   /   \   \   /  /    ^
  \ `-` /   \  `-'   /     \   `-`  /
jgs`-.-`     '.____.'       `.____.'

开始

Felix qui potuit rerum cognoscere causas -- 维吉尔

一切都始于 Samuele Pedroni 在 Python 开发邮件列表中的帖子 [1]。在他的帖子中,Samuele 表明 Python 2.2 方法解析顺序不是单调的,他建议将其替换为 C3 方法解析顺序。Guido 同意了他的观点,因此现在 Python 2.3 使用 C3。 C3 方法本身与 Python 无关,因为它是由 Dylan 的开发人员发明的,并且在一篇面向 Lisp 程序员的论文中进行了描述 [2]。本文给出了对 C3 算法的(希望)易读的讨论,以供希望了解更改原因的 Python 爱好者参考。

首先,我想指出,我接下来要说的仅适用于 Python 2.2 中引入的新式类经典类保留其旧的方法解析顺序,即深度优先,然后从左到右。因此,对于经典类,不会破坏旧代码;即使原则上可能会破坏 Python 2.2 新式类的代码,但在实践中,C3 解析顺序与 Python 2.2 方法解析顺序不同的情况非常罕见,因此预计不会真正破坏代码。因此

不要害怕!

此外,除非您大量使用多重继承并且具有非平凡的层次结构,否则您无需了解 C3 算法,并且可以轻松跳过本文。另一方面,如果您真的想知道多重继承是如何工作的,那么本文适合您。好消息是事情并不像您想象的那么复杂。

让我从一些基本定义开始。

  1. 给定一个复杂的,具有多重继承层次结构的类 C,指定方法被重写的顺序,即指定 C 的祖先的顺序,是一项非平凡的任务。
  2. 类 C 的祖先列表(包括类本身),按照从最近的祖先到最远的祖先的顺序排列,称为类优先级列表或 C 的线性化
  3. 方法解析顺序(MRO)是构造线性化的一组规则。在 Python 文献中,“C 的 MRO”这个习惯用法也用作类 C 线性化的同义词。
  4. 例如,在单继承层次结构的情况下,如果 C 是 C1 的子类,而 C1 是 C2 的子类,则 C 的线性化就是列表 [C, C1, C2]。但是,对于多重继承层次结构,线性化的构造更加麻烦,因为它更难以构造既尊重本地优先级排序又尊重单调性的线性化。
  5. 我将在稍后讨论本地优先级排序,但在这里我可以给出单调性的定义。当满足以下条件时,MRO 是单调的:如果 C1 在 C 的线性化中位于 C2 之前,则 C1 在 C 的任何子类的线性化中都位于 C2 之前。否则,派生新类的无害操作可能会更改方法的解析顺序,从而可能引入非常细微的错误。稍后将展示发生这种情况的示例。
  6. 并非所有类都允许线性化。在复杂的层次结构中,在某些情况下,无法派生一个类,使其线性化尊重所有所需的属性。

这里我给出一个这种情况的例子。考虑层次结构

>>> O = object
>>> class X(O): pass
>>> class Y(O): pass
>>> class A(X,Y): pass
>>> class B(Y,X): pass

可以用以下继承图表示,其中我用 O 表示object类,它是新式类任何层次结构的开始

 -----------
|           |
|    O      |
|  /   \    |
 - X    Y  /
   |  / | /
   | /  |/
   A    B
   \   /
     ?

在这种情况下,不可能从 A 和 B 派生新类 C,因为 X 在 A 中位于 Y 之前,而 Y 在 B 中位于 X 之前,因此方法解析顺序在 C 中将是模棱两可的。

在这种情况下,Python 2.3 会引发异常(TypeError:基类 Y 和 X 之间存在 MRO 冲突),从而阻止幼稚的程序员创建模糊的层次结构。 Python 2.2 则不会引发异常,而是选择一个临时的排序(在本例中为 CABXYO)。


    _                   .-=-.          .-==-.
   { }      __        .' O o '.       /  -<' )
   { }    .' O'.     / o .-. O \     /  .--v`
   { }   / .-. o\   /O  /   \  o\   /O /
    \ `-` /   \ O`-'o  /     \  O`-`o /
jgs  `-.-`     '.____.'       `.____.'

C3 方法解析顺序

让我介绍一些简单的符号,这些符号将有助于以下讨论。我将使用快捷表示法

C1 C2 ... CN

表示类列表 [C1, C2, ..., CN]。

列表的头部是其第一个元素

head = C1

尾部是列表的其余部分

tail = C2 ... CN。

我还会使用表示法

C + (C1 C2 ... CN) = C C1 C2 ... CN

表示列表 [C] + [C1, C2, ..., CN] 的和。

现在我可以解释 Python 2.3 中 MRO 的工作原理。

考虑一个具有多重继承层次结构的类 C,其中 C 继承自基类 B1、B2、...、BN。我们要计算类 C 的线性化 L[C]。规则如下

C 的线性化是 C 加上父项线性化与父项列表的合并。

用符号表示法表示

L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)

特别是,如果 C 是object类,它没有父项,则线性化是微不足道的

L[object] = object。

但是,通常必须根据以下规则计算合并

取第一个列表的头部,即 L[B1][0];如果该头部不在任何其他列表的尾部中,则将其添加到 C 的线性化中,并将其从合并中的列表中删除,否则查看下一个列表的头部,如果这是一个好的头部,则取它。然后重复此操作,直到删除所有类或无法找到好的头部。在这种情况下,不可能构造合并,Python 2.3 将拒绝创建类 C 并引发异常。

此规则确保如果可以保留排序,则合并操作保留排序。另一方面,如果无法保留顺序(如上面讨论的严重顺序不一致的示例),则无法计算合并。

如果 C 只有一个父项(单继承),则合并的计算很简单;在这种情况下

L[C(B)] = C + merge(L[B],B) = C + L[B]

但是,在多重继承的情况下,事情更加麻烦,我不希望您在没有几个示例的情况下就能理解该规则 ;-)


          .-'-.
        /'     `\
      /' _.-.-._ `\
     |  (|)   (|)  |
     |   \__"__/   |
     \    |v.v|    /
      \   | | |   /
       `\ |=^-| /'
         `|=-=|'
          | - |
          |=  |
          |-=-|
    _.-=-=|= -|=-=-._
   (      |___|      )
  ( `-=-=-=-=-=-=-=-` )
  (`-=-=-=-=-=-=-=-=-`)
  (`-=-=-=-=-=-=-=-=-`)
   (`-=-=-=-=-=-=-=-`)
    (`-=-=-=-=-=-=-`)
jgs  `-=-=-=-=-=-=-`

示例

第一个示例。考虑以下层次结构

>>> O = object
>>> class F(O): pass
>>> class E(O): pass
>>> class D(O): pass
>>> class C(D,F): pass
>>> class B(D,E): pass
>>> class A(B,C): pass

在这种情况下,继承图可以绘制为

                          6
                         ---
Level 3                 | O |                  (more general)
                      /  ---  \
                     /    |    \                      |
                    /     |     \                     |
                   /      |      \                    |
                  ---    ---    ---                   |
Level 2        3 | D | 4| E |  | F | 5                |
                  ---    ---    ---                   |
                   \  \ _ /       |                   |
                    \    / \ _    |                   |
                     \  /      \  |                   |
                      ---      ---                    |
Level 1            1 | B |    | C | 2                 |
                      ---      ---                    |
                        \      /                      |
                         \    /                      \ /
                           ---
Level 0                 0 | A |                (more specialized)
                           ---

O、D、E 和 F 的线性化是微不足道的

L[O] = O
L[D] = D O
L[E] = E O
L[F] = F O

B 的线性化可以计算为

L[B] = B + merge(DO, EO, DE)

我们看到 D 是一个好的头部,因此我们取它,并且我们简化为计算merge(O,EO,E)。现在 O 不是一个好的头部,因为它在序列 EO 的尾部中。在这种情况下,规则说我们必须跳到下一个序列。然后我们看到 E 是一个好的头部;我们取它,并且我们简化为计算merge(O,O)得到 O。因此

L[B] =  B D E O

使用相同的过程,可以找到

L[C] = C + merge(DO,FO,DF)
     = C + D + merge(O,FO,F)
     = C + D + F + merge(O,O)
     = C D F O

现在我们可以计算

L[A] = A + merge(BDEO,CDFO,BC)
     = A + B + merge(DEO,CDFO,C)
     = A + B + C + merge(DEO,DFO)
     = A + B + C + D + merge(EO,FO)
     = A + B + C + D + E + merge(O,FO)
     = A + B + C + D + E + F + merge(O,O)
     = A B C D E F O

在此示例中,线性化按照继承级别以非常好的方式排序,从某种意义上说,较低级别(即更专业的类)具有更高的优先级(请参见继承图)。但是,这并非普遍情况。

我留给读者练习计算我的第二个示例的线性化

>>> O = object
>>> class F(O): pass
>>> class E(O): pass
>>> class D(O): pass
>>> class C(D,F): pass
>>> class B(E,D): pass
>>> class A(B,C): pass

与上一个示例的唯一区别是更改了 B(D,E) --> B(E,D);然而,即使是如此小的修改,也会完全改变层次结构的排序

                           6
                          ---
Level 3                  | O |
                       /  ---  \
                      /    |    \
                     /     |     \
                    /      |      \
                  ---     ---    ---
Level 2        2 | E | 4 | D |  | F | 5
                  ---     ---    ---
                   \      / \     /
                    \    /   \   /
                     \  /     \ /
                      ---     ---
Level 1            1 | B |   | C | 3
                      ---     ---
                       \       /
                        \     /
                          ---
Level 0                0 | A |
                          ---

请注意,位于层次结构第二级的类 E 优先于位于层次结构第一级的类 C,即 E 比 C 更专业,即使它处于更高的级别。

一个懒惰的程序员可以直接从 Python 2.2 中获取 MRO,因为在这种情况下,它与 Python 2.3 线性化一致。只需调用类 A 的 .mro() 方法即可

>>> A.mro()
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>,
<class '__main__.C'>, <class '__main__.D'>, <class '__main__.F'>,
<type 'object'>)

最后,让我考虑第一节中讨论的涉及严重顺序不一致的示例。在这种情况下,可以很容易地计算出 O、X、Y、A 和 B 的线性化

L[O] = 0
L[X] = X O
L[Y] = Y O
L[A] = A X Y O
L[B] = B Y X O

然而,对于继承自 A 和 B 的类 C,不可能计算出线性化。

L[C] = C + merge(AXYO, BYXO, AB)
     = C + A + merge(XYO, BYXO, B)
     = C + A + B + merge(XYO, YXO)

此时,我们无法合并列表 XYO 和 YXO,因为 X 在 YXO 的尾部,而 Y 在 XYO 的尾部:因此没有好的头部,C3 算法停止。Python 2.3 会引发错误,并拒绝创建类 C。


                      __
    (\   .-.   .-.   /_")
     \\_//^\\_//^\\_//
jgs   `"`   `"`   `"`

错误的方法解析顺序

当 MRO 破坏诸如局部优先级排序和单调性等基本属性时,它就是错误的。在本节中,我将展示经典类的 MRO 和 Python 2.2 中新式类的 MRO 都是错误的。

从局部优先级排序开始更容易。考虑以下示例

>>> F=type('Food',(),{'remember2buy':'spam'})
>>> E=type('Eggs',(F,),{'remember2buy':'eggs'})
>>> G=type('GoodFood',(F,E),{}) # under Python 2.3 this is an error!

带有继承图

             O
             |
(buy spam)   F
             | \
             | E   (buy eggs)
             | /
             G

      (buy eggs or spam ?)

我们看到类 G 继承自 F 和 E,其中 F 在 E 之前:因此我们期望属性 G.remember2buyF.rembermer2buy 继承,而不是由 E.remember2buy 继承:然而 Python 2.2 给出的结果是

>>> G.remember2buy
'eggs'

这是对局部优先级排序的破坏,因为局部优先级列表中的顺序,即 G 的父类列表,在 Python 2.2 的 G 的线性化中没有保留

L[G,P22]= G E F object   # F *follows* E

有人可能会争辩说,F 在 Python 2.2 线性化中跟随 E 的原因是 F 的专业化程度低于 E,因为 F 是 E 的超类;然而,破坏局部优先级排序是相当不直观且容易出错的。尤其是它与旧式类不同

>>> class F: remember2buy='spam'
>>> class E(F): remember2buy='eggs'
>>> class G(F,E): pass
>>> G.remember2buy
'spam'

在这种情况下,MRO 是 GFEF,并且保留了局部优先级排序。

一般来说,应该避免像前面那样的层次结构,因为它不清楚 F 应该覆盖 E 还是反之。Python 2.3 通过在创建类 G 时引发异常来解决歧义,有效地阻止程序员生成有歧义的层次结构。原因是当合并

merge(FO,EFO,FE)

无法计算时,C3 算法会失败,因为 F 在 EFO 的尾部,而 E 在 FE 的尾部。

真正的解决方案是设计一个无歧义的层次结构,即从 E 和 F(更具体的先)而不是从 F 和 E 派生 G;在这种情况下,MRO 是 GEF,毫无疑问。

           O
           |
           F (spam)
         / |
(eggs)   E |
         \ |
           G
             (eggs, no doubt)

Python 2.3 强制程序员编写良好的层次结构(或者至少是不太容易出错的层次结构)。

另外,我想指出,Python 2.3 算法足够聪明,可以识别明显的错误,例如父类列表中类的重复

>>> class A(object): pass
>>> class C(A,A): pass # error
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: duplicate base class A

在这种情况下,Python 2.2(对于经典类和新式类)都不会引发任何异常。

最后,我想指出我们从这个例子中学到的两个教训

  1. 尽管名称如此,MRO 决定的是属性的解析顺序,而不仅仅是方法的解析顺序;
  2. Pythonistas 的默认食物是 spam!(但你已经知道了 ;-)

                      __
    (\   .-.   .-.   /_")
     \\_//^\\_//^\\_//
jgs   `"`   `"`   `"`

在讨论了局部优先级排序的问题之后,现在让我考虑一下单调性的问题。我的目标是证明经典类的 MRO 和 Python 2.2 新式类的 MRO 都不是单调的。

要证明经典类的 MRO 是非单调的相当简单,只需查看钻石图即可

   C
  / \
 /   \
A     B
 \   /
  \ /
   D

人们很容易辨别出不一致之处

L[B,P21] = B C        # B precedes C : B's methods win
L[D,P21] = D A C B C  # B follows C  : C's methods win!

另一方面,Python 2.2 和 2.3 的 MRO 没有问题,它们都给出

L[D] = D A B C

Guido 在他的文章 [3] 中指出,经典 MRO 在实践中并不那么糟糕,因为人们通常可以避免经典类的钻石继承。但是所有新式类都继承自object因此,钻石继承是不可避免的,并且不一致之处会在每个多重继承图中出现。

Python 2.2 的 MRO 使得打破单调性变得困难,但并非不可能。以下示例(最初由 Samuele Pedroni 提供)表明 Python 2.2 的 MRO 是非单调的

>>> class A(object): pass
>>> class B(object): pass
>>> class C(object): pass
>>> class D(object): pass
>>> class E(object): pass
>>> class K1(A,B,C): pass
>>> class K2(D,B,E): pass
>>> class K3(D,A):   pass
>>> class Z(K1,K2,K3): pass

以下是根据 C3 MRO 的线性化(读者应自行验证这些线性化并绘制继承图 ;-))

L[A] = A O
L[B] = B O
L[C] = C O
L[D] = D O
L[E] = E O
L[K1]= K1 A B C O
L[K2]= K2 D B E O
L[K3]= K3 D A O
L[Z] = Z K1 K2 K3 D A B C E O

Python 2.2 为 A、B、C、D、E、K1、K2 和 K3 提供了完全相同的线性化,但 Z 的线性化不同

L[Z,P22] = Z K1 K3 A K2 D B C E O

很明显,这种线性化是错误的,因为 A 在 D 之前,而在 K3 的线性化中 A 在 D 之后。换句话说,在 K3 中,从 D 派生的方法会覆盖从 A 派生的方法,但在 Z 中,它仍然是 K3 的子类,从 A 派生的方法会覆盖从 D 派生的方法!这是对单调性的违反。此外,Z 的 Python 2.2 线性化也与局部优先级排序不一致,因为类 Z 的局部优先级列表是 [K1, K2, K3] (K2 在 K3 之前),而在 Z 的线性化中,K2 K3 之后。这些问题解释了为什么 2.2 规则被放弃而支持 C3 规则。


                                                         __
   (\   .-.   .-.   .-.   .-.   .-.   .-.   .-.   .-.   /_")
    \\_//^\\_//^\\_//^\\_//^\\_//^\\_//^\\_//^\\_//^\\_//
jgs  `"`   `"`   `"`   `"`   `"`   `"`   `"`   `"`   `"`

结束

本节适用于没有耐心阅读前面所有章节,直接跳到结尾的读者。本节也适用于不想动脑筋的懒惰程序员。最后,它也适用于有些自负的程序员,否则他/她不会阅读关于多重继承层次结构中 C3 方法解析顺序的论文 ;-) 这三种美德加在一起(而不是分开)值得奖励:奖励是一个简短的 Python 2.2 脚本,使你可以在不损害大脑的情况下计算 2.3 MRO。只需更改最后一行即可使用我在本文中讨论的各种示例。

#<mro.py>

"""C3 algorithm by Samuele Pedroni (with readability enhanced by me)."""

class __metaclass__(type):
    "All classes are metamagically modified to be nicely printed"
    __repr__ = lambda cls: cls.__name__

class ex_2:
    "Serious order disagreement" #From Guido
    class O: pass
    class X(O): pass
    class Y(O): pass
    class A(X,Y): pass
    class B(Y,X): pass
    try:
        class Z(A,B): pass #creates Z(A,B) in Python 2.2
    except TypeError:
        pass # Z(A,B) cannot be created in Python 2.3

class ex_5:
    "My first example"
    class O: pass
    class F(O): pass
    class E(O): pass
    class D(O): pass
    class C(D,F): pass
    class B(D,E): pass
    class A(B,C): pass

class ex_6:
    "My second example"
    class O: pass
    class F(O): pass
    class E(O): pass
    class D(O): pass
    class C(D,F): pass
    class B(E,D): pass
    class A(B,C): pass

class ex_9:
    "Difference between Python 2.2 MRO and C3" #From Samuele
    class O: pass
    class A(O): pass
    class B(O): pass
    class C(O): pass
    class D(O): pass
    class E(O): pass
    class K1(A,B,C): pass
    class K2(D,B,E): pass
    class K3(D,A): pass
    class Z(K1,K2,K3): pass

def merge(seqs):
    print '\n\nCPL[%s]=%s' % (seqs[0][0],seqs),
    res = []; i=0
    while 1:
      nonemptyseqs=[seq for seq in seqs if seq]
      if not nonemptyseqs: return res
      i+=1; print '\n',i,'round: candidates...',
      for seq in nonemptyseqs: # find merge candidates among seq heads
          cand = seq[0]; print ' ',cand,
          nothead=[s for s in nonemptyseqs if cand in s[1:]]
          if nothead: cand=None #reject candidate
          else: break
      if not cand: raise "Inconsistent hierarchy"
      res.append(cand)
      for seq in nonemptyseqs: # remove cand
          if seq[0] == cand: del seq[0]

def mro(C):
    "Compute the class precedence list (mro) according to C3"
    return merge([[C]]+map(mro,C.__bases__)+[list(C.__bases__)])

def print_mro(C):
    print '\nMRO[%s]=%s' % (C,mro(C))
    print '\nP22 MRO[%s]=%s' % (C,C.mro())

print_mro(ex_9.Z)

#</mro.py>

伙计们,就这些了,

享受吧!

    __
   ("_\   .-.   .-.   .-.   .-.   .-.   .-.   .-.   .-.   /)
      \\_//^\\_//^\\_//^\\_//^\\_//^\\_//^\\_//^\\_//^\\_//
jgs    `"`   `"`   `"`   `"`   `"`   `"`   `"`   `"`   `"`

资源

[1]Samuele Pedroni 在 python-dev 上发起的帖子:http://mail.python.org/pipermail/python-dev/2002-October/029035.html
[2]论文《Dylan 的单调超类线性化》:https://doi.org/10.1145/236337.236343
[3]Guido van Rossum 的文章《统一 Python 2.2 中的类型和类》:https://pythonlang.cn/2.2.2/descrintro.html