Pedro
Pedro

Reputation: 385

Decorating a child class's __init__ method with super()

My class hierarchy is set up so that every child's __init__() must set self._init_has_run() to False, call the parent's __init__(), then do their own __init__(), and finally set self._init_has_run() to True. I have the following code:

class Parent:
    def __init__(self, arg1, arg2):
        pass  # do stuff

    def init(cls, fun):
        def decorated_init(self, *args, **kwargs):
            self._init_has_run = False
            x = super()
            super().__init__(*args, **kwargs)
            fun(self, *args, **kwargs)
            self._init_has_run = True
        return decorated_init

class Child(Parent):
    @Parent.init
    def __init__(self, arg1, arg2):
        pass  # do stuff

Since there are a number of subclasses that follow the same general pattern for __init__(), and I can't figure out how to use metaclasses, I am using a decorator to consolidate the repetitive logic and then just applying that decorator to all descendant __init__() methods.

Python is throwing the following:

File "filename.py", line 82, in decorated_init
super().__init__(*args, **kwargs)
TypeError: object.__init__() takes no parameters

I confirmed through the debugger that the toggling of self._init_has_run works fine and super() is resolving to the Parent class, but when the decorator tries to call super().__init__(*args, **kwargs), why does Python try to call object.__init__() instead?

Upvotes: 4

Views: 1892

Answers (1)

Andrew Dunai
Andrew Dunai

Reputation: 3129

You can easily use metaclasses to do some pre/post-init stuff. Consider this example:

class Meta(type):
    def __new__(meta, *args):
        # This is something like 'class constructor'.
        # It is called once for every new class definition.
        # It sets default value of '_init_has_run' for all new objects.
        # This is analog to `class Foo: _init_has_run = False`:
        # new objects will all have _init_has_run set to False by default.

        cls = super(Parent, meta).__new__(meta, *args)
        cls._init_has_run = False
        return cls

    def __call__(cls, *args, **kwargs):
        # This is called each time you create new object.
        # It will run new object's constructor
        # and change _init_has_run to False.

        obj = type.__call__(cls, *args, **kwargs)
        obj._init_has_run = True
        return obj


class Child:
    __metaclass__ = Meta

    def __init__(self):
        print 'init:', self._init_has_run

    def foo(self):
        print 'foo:', self._init_has_run


a = Child()
a.foo()

a = Child()
a.foo()

Output:

init: False
foo: True
init: False
foo: True

Hope this helps!

Upvotes: 3

Related Questions