Python2 的 __bases__ 和 super

1 投票
2 回答
599 浏览
提问于 2025-04-17 20:48

在 Python 2.7 中,我想从某个类 E 重建到根类 A 的继承链。下面有一个菱形继承的问题,但我只对一条路径感兴趣,而不是所有路径,所以应该可以这样做。虽然我不确定这样做是否合适,但现在我只想知道我哪里理解错了……

class A(object):
    @classmethod
    def supah(thisCls):
        return [cls for cls in thisCls.__bases__ if issubclass(cls, A)]
    def name(self):
        return 'A'

class C(A):
    def name(self):
        return 'C'

class D(A):
    def name(self):
        return 'D'

class E(C, D):
    def name(self):
        return 'E'

current = E
while True:
    try:
        print current, super(current, E()), super(current, E()).name()
    except AttributeError:
        break
    current = current.supah()[0]

输出结果

<class '__main__.E'> <super: <class 'E'>, <E object>> C
<class '__main__.C'> <super: <class 'C'>, <E object>> D
<class '__main__.A'> <super: <class 'A'>, <E object>>

那为什么会有 D 呢?它在调用

super(C, E()).name()

这里的 super(C, E()) 应该是“类 A”,对吧?如果第一行的 C 是 D,我或许能理解,但在我看来,第二行肯定应该是 A。

谁能帮帮我?

编辑:我原本以为调用

super(C, obj).name()

会得到名字“A”,因为 C 的线性化是 [C, A, object]

然而,显然 super(C, obj).name() 并不是这个意思。它仍然使用 obj 的完整线性化:[E, C, D, A, object](感谢 @Martijn Pieters),只是从 C 开始(之后)。所以 D 出现在 A 之前。

2 个回答

0

@Martijn Pieters 的回答解释了观察到的结果是如何产生的。

如果你想得到我之前错误期待的从 super 得到的结果,可以参考 @Sven Marnach 在 python: super()-like proxy object that starts the MRO search at a specified class 上的接受答案,使用一种方法。

如果你想得到一个表现得像类 AC 实例:

class Delegate:
    def __init__(self, cls, obj):
        self._delegate_cls = cls
        self._delegate_obj = obj
    def __getattr__(self, name):
        x = getattr(self._delegate_cls, name)
        if hasattr(x, "__get__"):
            return x.__get__(self._delegate_obj)
        return x

这样可以从 A 获取 .name()

class C(A):
    def name(self):
        return delegate(A, self).name() + 'C'
C().name()
# result: AC

如果你对一个类似 super 的结构感兴趣,它可以获取(第一个)直接的祖先:

class parent:
    def __init__(self, cls, obj):
        if len(cls.__bases__):
            self._delegate_cls = cls.__bases__[0]
        else:
            raise Exception('parent called for class "%s", which has no base classes')
        self._delegate_obj = obj
    def __getattr__(self, name):
        x = getattr(self._delegate_cls, name)
        if hasattr(x, '__get__'):
            return x.__get__(self._delegate_obj)
        return x

这样调用:

class C(A):
    def name(self):
        return parent(C, self).name() + 'C'
print C().name()
# result: AC

我觉得没有办法不明确地包含当前类的名字,就像 super 在 Python 2 中那样。

请注意,这适用于特殊情况。例如,在我的例子中,如果 C 没有实现 .name(),它会调用 A 上的那个,而不是 D。不过,它确实让你从一个类到根类获取一个(而不是“那个”)直接的祖先链。此外,parent(Cls, obj) 将始终是 obj 的一个父类,而不是一个 Cls 完全不知道但恰好是 obj 祖先的类。

3

super() 这个东西不是看 __bases__,而是看方法解析顺序(MRO),通过 type(self).mro() 来确定。

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

你可以看到,D 在里面,因为它是 E 的一个基类;当你调用 super(C, E()).name() 时,D 就会在 MRO 中排在后面。

MRO 会包含所有基类,确保在类的层级结构中不会漏掉任何一个类;这样做是为了防止在菱形继承模式下出现跳过某些类的情况。

MRO 的工作原理在 The Python 2.3 Method Resolution Order 中有详细解释。

你也可以看看 Guido van Rossum 的解释;他用一个菱形模式来说明:

class A:
  def save(self): pass

class B(A): pass

class C(A):
  def save(self): pass

class D(B, C): pass

为什么 MRO 是重要的;当你调用 D().save() 时,你希望调用的是 C.save()(更具体的),而不是 A.save()

如果你真的想要从 C.name跳过 D,你需要明确地在 MRO 中找到 C.__bases__[0],然后告诉 super() 从那里开始寻找下一个 .name() 方法:

mro = type(self).mro()
preceding = mro[0]
for i, cls in enumerate(mro[1:], 1):
    if cls in self.__bases__:
        preceding = mro[i - 1]
name = super(preceding, self).name()

对于你的 E.mro() 和类 C,这会找到 D,因为它在 C 的第一个基类 A 之前。然后调用 super(D, self).name() 就会告诉 super() 找到第一个在 D 之后有 name() 方法的类,这里就是 A

撰写回答