3

I am trying to get a class decorator working. The decorator will add a __init_subclass__ method to the class it is applied to.

However, when the method is added to the class dynamically, the first argument is not being bound to the child class object. Why is this happening?

As an example: this works, and the static code below is an example of what I'm trying to end up with:

class C1:
    def __init_subclass__(subcls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)
        print(f"init_subclass -> {subcls.__name__}, {args!r}, {kwargs!r}")

Test:

>>> D = type("D", (C1,), {})
init_subclass -> D, (), {}

However if I add the __init__subclass__ method dynamically, the child class is not being bound to the first argument:

def init_subclass(subcls, **kwargs):
    super().__init_subclass__(**kwargs)
    print(f"init_subclass -> {subcls.__name__}, {args!r}, {kwargs!r}")

def decorator(Cls):
    Cls.__init_subclass__ = init_subclass
    return Cls

@decorator
class C2:
    pass

Test:

>>> D = type("D", (C2,), {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: init_subclass() missing 1 required positional argument: 'subcls'

Why is this happening and how can I do this and get the binding to work the right way?

Rick
  • 43,029
  • 15
  • 76
  • 119

2 Answers2

4

__init_subclass__ is an implicit classmethod.

It may not be possible to use zero-argument super (read here if you want to learn about why), but you should be able to explicitly bind the super inside the decorator itself.

def decorator(Cls):
    def __init_subclass__(subcls, **kwargs):
        print(f'init subclass {Cls!r}, {subcls!r}, {kwargs!r}')
        super(Cls, subcls).__init_subclass__(**kwargs)
    Cls.__init_subclass__ = classmethod(__init_subclass__)
    return Cls

@decorator
class C:
    pass

class D(C):
    pass
wim
  • 338,267
  • 99
  • 616
  • 750
  • i think i have an idea, but can you expound on the interpreter's need for the `subcls` argument to `super()`? i get a runtime error without it. shouldn't the argument be `C2`...? – Rick May 02 '18 at 15:32
  • Hmm, yes - technically it should be `super(C2, subcls)` there, I was sloppy. The way no-argument super works (using `__class__` cell) makes that tricky to do when the `Cls` doesn't exist yet, but I think you can use the closure in the decorator, see edit and let me know how it goes (untested). – wim May 02 '18 at 17:41
  • The closure works perfectly. It even works when built using `exec` and a string of code. – Rick May 02 '18 at 17:43
1

Just a comment to those that advocate for using abc. While abc also would solve the problem at stake, there are a two differences (that I know of) between the two approaches worth mentioning:

Class definition vs instantiation.

The abc.abstractmethod decorator enforces the constraint on the child class upon class instantiation, whilst the __init_subclass__ data model does it already at class definition. Example:

class Foo(abc.ABC):  
    def init(self):
        pass
    @abc.abstractmethod
    def foo():
        pass

class Bar(Foo):
    pass

This code will compile without a problem. The error will appear first when you call the constructor of the child class via e.g. x = Bar(). If this is library code, this means the error does not appear until runtime. On the other hand, the following code:

class Par():
    def __init_subclass__(cls, *args, **kwargs):
        must_have = 'foo'
        if must_have not in list(cls.__dict__.keys()):
            raise AttributeError(f"Must have {must_have}")
    def __init__(self):
        pass

class Chi(Par):
    def __init__(self):
        super().__init__()

will throw an error, because the check is performed at class definition time.

Enforcing through levels of inheritance

The other difference is that the abstractmethod wants the decorated method to be overwritten once, but the __init_subclass__ data model will also enforce the constraint to child of child classes. Example:

class Foo(abc.ABC):
    def __init__(self):
        pass

    @abc.abstractmethod
    def foo():
        pass

class Bar(Foo):
    def __init__(self):
        super().__init__()
    def foo(self):
        pass

class Mai(Bar):
    pass

x = Mai()

This code will work. Mai does not need a foo method since the abstract method has already been overwritten in Bar. On the other hand:

class Par():
    def __init_subclass__(cls, *args, **kwargs):
        must_have = 'foo'
        if must_have not in list(cls.__dict__.keys()):
            raise AttributeError(f"Must have {must_have}")
    def __init__(self):
        pass

class Chi(Par):
    def __init__(self):
        super().__init__()

    def foo(self):
        pass

class Chichi(Chi):
    def __init__(self):
        super().__init__()

This will throw an error, since Chichi ALSO has to have a foo method, even if a class in between has one.

mosegui
  • 664
  • 6
  • 15