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?

asked May 2, 2018 at 14:34

2 Answers 2

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
answered May 2, 2018 at 15:16
Sign up to request clarification or add additional context in comments.

3 Comments

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...?
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).
The closure works perfectly. It even works when built using exec and a string of code.
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.

answered Sep 5, 2019 at 16:23

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.