Pregunta

In Python 2.7, I am trying to reconstruct AN inheritance chain from a certain class E to the root A. There is a diamond inheritance problem as shown below, but I am interested in a path, not THE path, so it should work. Whether it's something I should be doing (this way) is questionable, but right now I just want to know what I am misunderstanding...

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]

The output

<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>>

What is D doing there? It is calling

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

where super(C, E()) should be "class A", right? If the C on the first line had been an D I would have (sort of) understood, but in my mind the second line should definitely be an A.

Any help?

EDIT: My understanding was that calling

super(C, obj).name()

would result in the name "A", because the linearization of C is [C, A, object].

However, this is not what super(C, obj).name() means apparently. It still uses the full linearization of obj: [E, C, D, A, object] (thanks to @Martijn Pieters), it just starts at (after) C. Therefore D comes before A.

¿Fue útil?

Solución

super() doesn't look at __bases__; it looks at the Method Resolution Order (MRO), through type(self).mro():

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

As you can see, D is in there, because it is a base class of E; when you call super(C, E()).name(), D comes next in the MRO.

The MRO will always include all base classes in a hierarchy; you cannot build a class hierarchy where the MRO could not be established. This to prevent classes being skipped in a diamond inheritance pattern.

How the MRO works is explained in detail in The Python 2.3 Method Resolution Order.

You may also want to read Guido van Rossum's explanation; he uses a diamond pattern with:

class A:
  def save(self): pass

class B(A): pass

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

class D(B, C): pass

to illustrate why an MRO is important; when calling D().save() you'd want C.save() to be invoked (more specialized), not A.save().

If you really wanted to skip D from C.name, you'd have to explicitly find C.__bases__[0] in the MRO, then tell super() to start search for the next .name() method from there:

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()

For your E.mro() and class C, this'll find D, as it precedes the first base class of C, A. Calling super(D, self).name() then tells super() to find the first class past D with a name() method, which is A here.

Otros consejos

The answer by @Martijn Pieters explains how the observed result is produced.

In case one wants to produce the result that I was incorrectly expecting from super, one might use an approach based on the accepted answer from @Sven Marnach on python: super()-like proxy object that starts the MRO search at a specified class

If you want to get something that behalves as a class A version of a C instance:

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

which can be used to get .name() from A like this:

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

If you are interested in a super-like construct that gets a (the first) direct ancestor:

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

called like this:

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

I don't think there is a way to not explicitly include the name of the current class, just like super (in py2).

Note that this is for special cases. E.g. in my example if C doesn't implement .name() it calls the one on A, never D. It does however let you get a (not 'the') direct ancestor line from a class to the root. Also, parent(Cls, obj) will always be a parent of obj, not a class that Cls known nothing about but which happens to be an ancestor of obj.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top