Add post processing logic on one method of all existing subclasses

59 views Asked by At

In my codebase I have tens of already existing sub classes and I want to hook extra processing code into one public method inherited and overridden by all of those sub classes.

So I am trying to use __init_subclass__ to do that, and this is the result:

def log(return_value):
  print('Logged:', return_value)


class Car:
  @staticmethod
  def logged_run(run): 

    def wrap(self):
      result = run(self)
      log(result)
      return result

    return wrap

  def run(self):
    return 'Car'
  
  def __init_subclass__(cls) -> None:
    super().__init_subclass__()
    cls.run = cls.logged_run(cls.run)

class AutoCar(Car):
  def run(self):
    return 'AutoCar'



auto = AutoCar()
car_type = auto.run()

Output:

Logged: AutoCar

I searched and did not find such usage for __init_subclass__, so is there anything wrong with such an implementation?

Do I need to call super __init__subclass_ as in my example super().__init_subclass__()?

Do I need **kwargs in the parameter list of __init_subclass__?

Finally, is there a better way to do it?

1

There are 1 answers

1
jsbueno On

So, yes - your usage will produce you the right results - and, yes, your code would be better if it included the provisions you are in doubt in __init_subclass__. That way your class hierarchy will not be broken - __init_subclass__ wise, if at any point someone on the same codebase decides to add some functionality via mixin classes there.

So, to sumarize: __init_subclass__ is automatically considered as a classmethod: no need for the decorator. And it will receive any kwargs passed as part of the class statement, but metaclass (it is consumed by the the class creation machinery) - and any non-recognized such parameters should be passed on to super().__init_subclass__:

  def __init_subclass__(cls, **kwargs):
    super().__init_subclass__(**kwargs)
    cls.run = cls.logged_run(cls.run)

whether your code for __init_subclass__ is before or after the call to super() does not matter for Python, and is up to you - it won't make any difference but if you have class initialisations designed to work in collaboratively.

It is also worth mentioning __init_subclass__ is called before the metaclass __init__ method, if there is any, in a custom metaclass.

Also, even if working with strict typing, I think it would be redundant to annotate the return type (None) of __init_subclass__: your tools should know about it, as it is in the language spec, and human readers of the code will just see there is no return statement (along with no typing errors or warnings)

Apart from the workings of __init_subclass__, one peculiarity of your task is that it will wrap each version of a given method in each subclass in your hierarchy - if the methods use a super() call to themselves, the decorator will run multiple-times.

This is not easy to overcome if undesired, and have to be done "the hard way": setting a state somewhere (usually in an instance attribute) to indicate the decorator has already "entered" and have an if guard clause at the decorator code so that it does not run more than once per call.