2

I'm trying to make a decorator that automatically calls the parent class init. It works with single inheritance but when I try to chain the effect I get a stack overflow error. Can anyone explain (a) Why this is happening and (b) How to accomplish what I want to accomplish?

def init(__init):
    def wrapper(self, *args, **kwargs):
        self.__class__.__bases__[0].__init__(self)
        __init(self)
    return wrapper

class Base:
    def __init__(self):
        print("base init")


class Parent(Base):
    @init
    def __init__(self):
        print("parent init")

class Child(Parent):
    @init
    def __init__(self):
        print("child init")

a = Parent() #works
#c = Child() #stack overflow
John K
  • 184
  • 6
  • 1
    Take a look at what `self.__class__` is in the second case. It will always be `Parent` because `self` never stops being an instance of `Child`, no matter what method you call on it. – Mad Physicist Nov 20 '17 at 17:43
  • You want to have a reference to the class that contains the `__init` that you are calling. That will ensure that your recursion eventually terminates. – Mad Physicist Nov 20 '17 at 17:44
  • what is the point of this decorator actually??? – bruno desthuilliers Nov 20 '17 at 17:45
  • 3
    Don't bother trying to make this decorator work. It's going to be a difficult journey full of traps. Just call the parent `__init__` in your constructor like everyone else. It's only a few characters longer than typing `@init` anyway. – Aran-Fey Nov 20 '17 at 17:47
  • @Rawing. I hope I've captured your sentiment in my answer. – Mad Physicist Nov 20 '17 at 17:56
  • @brunodesthuilliers I thought I could condense some of my boilerplate down to a single decorator. I was aware that when I'm starting to write things like "self.__class__.__bases__[0]" I'm probably off in the weeds but I was curious and wanted to learn what was going on – John K Nov 20 '17 at 18:33
  • @JohnK. Picking which method to call is not really boilerplate. If you have multiple parent classes, you have to make the decision as to which `__init__`s to call very carefully on a case-by-case basis. Using a decorator like that will not work at all. – Mad Physicist Nov 20 '17 at 18:36

1 Answers1

4

The Right Way

The correct way to do this would be to call the parent initializer in your code directly. In Python 3:

super().__init__()

In Python 2:

super(Parent, self).__init__()

or

super(Child, self).__init__()

The Error

To answer your immediate question though, the infinite recursion happens because of how you get the parent:

self.__class__.__bases__[0]

self will not stop being an instance of Child no matter how many times you call your wrapper function. You end up calling Parent.__init__(self) indefinitely because the parent class never changes.

The Wrong Way

What you probably want is to find the class that defined the __init__ method you are currently calling, and get the parent of that. @AlexMartelli has provided a function that does this or Python to in his answer, which I am copying verbatim here:

import inspect

def get_class_that_defined_method(meth):
    for cls in inspect.getmro(meth.im_class):
        if meth.__name__ in cls.__dict__: 
            return cls
    return None

I you are using Python 3, use @Yoel's version instead:

def get_class_that_defined_method(meth):
    if inspect.ismethod(meth):
        for cls in inspect.getmro(meth.__self__.__class__):
           if cls.__dict__.get(meth.__name__) is meth:
                return cls
        meth = meth.__func__  # fallback to __qualname__ parsing
    if inspect.isfunction(meth):
        cls = getattr(inspect.getmodule(meth),
                      meth.__qualname__.split('.<locals>', 1)[0].rsplit('.', 1)[0])
        if isinstance(cls, type):
            return cls
    return getattr(meth, '__objclass__', None)  # handle special descriptor objects

You can now redefine your init function as follows:

def init(__init):
    def wrapper(self, *args, **kwargs):
        get_class_that_defined_method(__init).__bases__[0].__init__(self)
        __init(self)
    return wrapper

Again. Please do not do this. There are probably many corner cases that I have not covered that will come and bite you, starting with everything around __bases__[0]. Just use super instead. It is a much better thought out version of the same idea anyway.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264