This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

Author ncoghlan
Recipients Martin.Teichmann, berker.peksag, gvanrossum, ncoghlan
Date 2016-07-24.05:15:01
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1469337302.41.0.00207886823295.issue27366@psf.upfronthosting.co.za>
In-reply-to
Content
I started reviewing Martin's patch, and I initially thought I had found a problem with the way __init_subclass__ is currently defined. It turned out I was wrong about it actually being broken, but I *do* now think it's inherently confusing, and we may be able to do something different that's more obviously correct (or at least easier to document - it was proposing revisions to the documentation that got me thinking along this path).

Specifically, I was thinking using super() in either the zero argument form or the explicit form could create an infinite loop due to the way we're currently proposing to interact with the MRO. Consider:

    class BaseClass:
        @classmethod
        def __init_subclass__(cls):
            super(cls, BaseClass).__init_subclass__()

    class SubClass(BaseClass):
        pass

If the initial call made by type.__new__() is effectively "SubClass.mro()[1].__init_subclass__()", then the super() call is going to call BaseClass.__init_subclass__ again.

However, it turned out I was wrong, as that's not what happens: the call made by the type machinery is instead "super(SubClass, SubClass).__init_subclass__", which gets it to the right place in the MRO and causes further super() calls to do the right thing.

However, the "more obviously correct" signature that occurred to me was to do this instead:

    class BaseClass:
        @classmethod
        def __init_subclass__(cls, subcls):
            super(cls, BaseClass).__init_subclass__(subcls)

    class SubClass(BaseClass):
        pass

Then the invocation from type.__new__ could be defined more simply as:

    SubClass.mro()[1].__init_subclass__(SubClass)

In all cases then (regardless of where you were in the MRO), "cls" would refer to "the class first in the MRO after the class being defined" and "subcls" would refer to "the class currently being defined".

If you consider the plugin example in the PEP, with the revised signature, it would look like:

    class PluginBase:
        subclasses = []

        def __init_subclass__(cls, subcls, **kwargs):
            super().__init_subclass__(**kwargs)
            cls.subclasses.append(subcls)

And *even if the subclass being defined shadowed the "subclasses" attribute*, this initialisation would still work. (You can still get yourself in trouble if a subclass somewhere else in the MRO shadows the attribute, but that's life in complex type hierarchies)

In the version in the PEP, the fact that "cls" is actually a subclass, and we're relying on the MRO to find "subclasses" is a really subtle implementation detail, while having two parameters makes it clear that "the class defining __init_subclass__" is distinct from the "new subclass being defined".
History
Date User Action Args
2016-07-24 05:15:02ncoghlansetrecipients: + ncoghlan, gvanrossum, berker.peksag, Martin.Teichmann
2016-07-24 05:15:02ncoghlansetmessageid: <1469337302.41.0.00207886823295.issue27366@psf.upfronthosting.co.za>
2016-07-24 05:15:02ncoghlanlinkissue27366 messages
2016-07-24 05:15:01ncoghlancreate