简体   繁体   中英

Patching __init_subclass__

I am having trouble patching a custom class' __init_subclass__ . I think it has to do with the way that I am binding the patched function to the class:

def _patched_initsubclass(cls, **kwargs):
    print(f"CLS from subclassing A: {cls}")
    super(cls, cls).__init_subclass__(**kwargs)

class A: ...

A.__init_subclass__ = _patched_initsubclass.__get__(A, A)

class B(A): ...  # Output: CLS from subclassing A: <class '__main__.A'>

However, I know that a properly set __init_subclass__ should have a different output:

class F:

    def __init_subclass__(cls, **kwargs):
        print(f"CLS from subclassing F: {cls}")
        pass

class C(F): ...  # Output: CLS from subclassing F: <class '__main__.C'>

ie cls in the super class' __init_subclass__ definition should be the subclass when subclassed. I've tried to find the correct way to bind the dunder method through different SO posts plus the docs , but haven't been able to find the correct way to do it.

Your use of super is invalid; it's supposed to be passed the type of the class that it's being called from (eg the class it was defined in) and the actual type it was passed (the class it was called on), so super(cls, cls) is lying; your explicit use of the descriptor protocol function __get__ prebound it to A (bypassing the descriptor protocol when it's invoked on B ), so it always says "I'm being called from A with an A " even when it's actually invoked on something else.

What you want isn't easy to do the correct way; your approach of making it a classmethod (which means it actually gets B , not A , as expected) and calling super(cls, None) , is still wrong, even if it happens to work by coincidence here. You're telling super to traverse the MRO of the None object and call the first __init_subclass__ it finds after B in the MRO. Apparently, even though B isn't in the MRO (which should be an error according to the docs : "If the second argument is an object, isinstance(obj, type) must be true. If the second argument is a type, issubclass(type2, type) must be true."; c'est la vie), it silently returns object.__init_subclass__ and calls it; it works only because object.__init_subclass__ doesn't do anything and doesn't object to being called.

The only way to do this correctly is to make a new version of the _patched_initsubclass for each class to patch that knows which class it's patching. Bonus, while doing this, you can make the closure in a way that enables zero-arg super() by putting __class__ in the closure scope for the new method (zero-arg super() magic is accomplished by the compiler making all functions defined in a class that reference __class__ or super actually closures with __class__ visible in closure scope).

An example solution would be:

def make_patched_initsubclass_for(__class__):  # Receive class to patch as __class__ directly
    # Same as before, just defined inside function to get closure scope,
    # and super() is called with no arguments
    def _patched_initsubclass(cls, **kwargs):
        print(f"CLS from subclassing A: {cls}")
        super().__init_subclass__(**kwargs)    # super() roughly equivalent to super(__class__, cls)

    # Returns a classmethod so it descriptor protocol
    # knows to provide class uniformly, never an instance
    return classmethod(_patched_initsubclass)

class A: ...

A.__init_subclass__ = make_patched_initsubclass_for(A)  # Produces valid closure for binding to A

class B(A): ...  # Output CLS from subclassing A: <class '__main__.B'>

If you didn't name the argument to make_patched_initsubclass_for __class__ (named it patched_cls or somesuch), you'd have to use super(patched_cls, cls) instead of super() , but it would work either way.

I found a solution that does not involve binding the pathed function through __get__ :

def _patched_initsubclass(cls, **kwargs):
    print(f"CLS from subclassing A: {cls}")

    super(cls, None).__init_subclass__(**kwargs)


class A: ...


A.__init_subclass__ = classmethod(_patched_initsubclass)


class B(A): ...  # Output CLS from subclassing A: <class '__main__.B'>

I am still not clear on why this works: ie what is difference between classmethod() and directly binding with __get__ .

The answer probably has to do with what classmethod does under the hood, so I will look into that.

I will leave this answer up for anyone else that might find it helpful, and will include any follow up info.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM