Using `super()` within `__init_subclass__` doesn't find parent's classmethod [duplicate]
Asked Answered
D

1

8

I try to access the classmethod of a parent from within __init_subclass__ however that doesn't seem to work. Suppose the following example code:

class Foo:
    def __init_subclass__(cls):
        print('init', cls, cls.__mro__)
        super(cls).foo()

    @classmethod
    def foo(cls):
        print('foo')


class Bar(Foo):
    pass

which produces the following exception:

AttributeError: 'super' object has no attribute 'foo'

The cls.__mro__ however shows that Foo is a part of it: (<class '__main__.Bar'>, <class '__main__.Foo'>, <class 'object'>).

So I don't understand why super(cls).foo() doesn't dispatch to Foo.foo. Can someone explain this?

Descant answered 18/4, 2018 at 0:7 Comment(4)
Change super(cls) to super(cls, cls). With the single-argument form, you end up looking for a foo method in the parent class of Foo (which is object).Armagnac
Why are you using super(cls) here? Unbound supers are rarely useful, and they aren't what you're looking for here. If you're just trying to call the parent class of cls's foo classmethod, that's super(cls, cls).foo(), just like you'd do with an instance for an method. If you're trying to do something different… then what?Molini
And if you actually want an explanation of why this doesn't do what you want, you'll need some background before it can be explained. Do you know what descriptors are, how methods work in general, and how super works in general, and just need to know how unbound supers work? Or do you need one of those more fundamental parts explained?Molini
@Molini Admittedly I have never used unbound super before and probably got confused by the docs: super([type[, object-or-type]]) Return a proxy object that delegates method calls to a parent or sibling class of type. [...]. So I probably need a refresher on this; the rest I am familiar with. But in the end it makes sense what you mentioned.Descant
M
14

A normal super object (what you normally get from calling super(MyType, self) or super() or super(MyType, myobj)) keeps track of both the type and the object it was created with. Whenever you look up an attribute on the super, it skips over MyType in the method resolution order, but if it finds a method it binds it to that self object.

An unbound super has no self object. So, super(cls) skips over cls in the MRO to find the method foo, and then binds it to… oops, it has nothing to call it on.

So, what things can you call a classmethod on? The class itself, or a subclass of it, or an instance of that class or subclass. So, any of those will work as the second argument to super here, the most obvious one being:

super(cls, cls)

This is somewhat similar to the difference between staticmethods (bound staticmethods are actually bound to nothing) and classmethods (bound classmethods are bound to the class instead of an instance), but it's not quite that simple.


If you want to know why an unbound super doesn't work, you have to understand what an unbound super really is. Unfortunately, the only explanation in the docs is:

If the second argument is omitted, the super object returned is unbound.

What does this mean? Well, you can try to work it out from first principles as a parallel to what it means for a method to be unbound (except, of course, that unbound methods aren't a thing in modern Python), or you can read the C source, or the original introduction to 2.2's class-type unification (including a pure-Python super clone).

A super object has a __self__ attribute, just like a method object. And super(cls) is missing its __self__, just like str.split is.1

You can't use an unbound super explicitly the way you can with an unbound method (e.g., str.split('123', '2') does the same as '123'.split('2'), but super(cls).foo(cls) doesn't work the same as super(cls, cls).foo()). But you can use them implicitly, the same way you do with unbound methods all the time without normally thinking about it.

If you don't know how methods work, the tl'dr is: when you evaluate myobj.mymeth, Python looks up mymeth, doesn't find it on myobj itself, but does find it on the type, so it checks whether it's a non-data descriptor, and, if so, calls its __get__ method to bind it to myobj.

So, unbound methods2 are non-data descriptors whose __get__ method returns a bound method. Unbound @classmethods are similar, but their __get__ ignores the object and returns a bound method bound to the class. And so on.

And unbound supers are non-data descriptors whose __get__ method returns a bound super.


Example (credit to wim for coming up with the closest thing to a use for unbound super that I've seen):

class A:
    def f(self): print('A.f')
class B(A):
    def f(self): print('B.f')
b = B()
bs = super(B)
B.bs = bs
b.bs.f()

We created an unbound super bs, stuck it on the type B, and then b.bs is a normal bound super, so b.bs.f is A.f, just like super().f would have been inside a B method.

Why would you want to do that? I'm not sure. I've written all kinds of ridiculously dynamic and reflective code in Python (e.g., for transparent proxies to other interpreters), and I can't remember ever needing an unbound super. But if you ever need it, it's there.


1. I'm cheating a bit here. First, unbound methods aren't a thing anymore in Python 3—but functions work the same way, so Python uses them where it used to use unbound methods. Second, str.split, being a C builtin, wasn't properly an unbound method even in 2.x—but it acts like one anyway, at least as far as we're concerned here.

2. Actually plain-old functions.

Molini answered 18/4, 2018 at 2:13 Comment(4)
Thanks for the thorough explanation. Especially your last comment "And unbound supers are non-data descriptors whose __get__ method returns a bound super." made it click.Descant
@Descant Yeah, that's really the key—"an unbound X" means "X.__get__ returns a bound X". Except that general rule isn't stated anywhere, and the only special case of it that was documented anywhere no longer even exists in Python 3, so… not the most discoverable of rules. By the way, I think there ought to be a better way to organize this answer so it'll help more people, but I can't think of what it is; if you have any ideas, that would be great.Molini
Maybe that's something for a more general question about the interplay between bound and unbound super? There's this SO question about unbound super but the answers are completely off; no other SO resource is really dealing with that topic. But in general I think I'd start with explaining MRO and motivating the usage of (bound) super and then switch to unbound super, explaining how it's created, what it is (along with descriptors), conversion to bound super, what's its purpose and some use cases (though those seem to be hard to find).Descant
@Descant That would definitely be a lot clearer but it seems like it might be way too long for an SO answer (especially with my verbosity). It might make an interesting blog post—but without some use cases for unbound supers, it's still pretty hard to motivate. As for its purpose, I suspect it's might just be there because it was trivial to implement once you've built descriptors and super for Python 2.2, so Guido just implemented it without worrying about whether there was a use case. That probably wouldn't happen in modern Python, but things were different back then.Molini

© 2022 - 2024 — McMap. All rights reserved.