maggie
maggie

Reputation: 4395

Decorators on classmethods

I have child classes which inherit some basic functionality from a parent class. The child classes shall have a generic constructor prepare_and_connect_constructor() which does some magic around the object creation of the parent class. For simplicity, the magic is done by a simple function based decorator (ultimately, it should be a part of the parent class).

def decorate_with_some_magic(func):
    def prepare_and_connect(*args, **kwargs):
        print("prepare something")
        print("create an object")
        obj = func(*args, **kwargs)
        print("connect obj to something")
        return obj

    return prepare_and_connect


class Parent:

    def __init__(self, a):
        self.a = a

    def __repr__(self):
        return f"{self.a}"


class Child(Parent):

    @classmethod
    @decorate_with_some_magic
    def prepare_and_connect_constructor(cls, a, b):
        """ use the generic connection decorator right on object creation """
        obj = super().__init__(a)
        # put some more specific attributes (over the parents class)
        obj.b = b
        return obj

    def __init__(self, a, b):
        """ init without connecting """
        super().__init__(a)
        self.b = b

    def __repr__(self):
        return f"{self.a}, {self.b}"


if __name__ == '__main__':
    print(Child.prepare_and_connect_constructor("special child", "needs some help"))

Using this code i finally get

obj = super().__init__(a)
  TypeError: __init__() missing 1 required positional argument: 'a'

when running prepare_and_connect_constructor().

Actually I would expect that the super.__init__(a) call should be the same as in Child.__init__. I guess the reason is related to the classmethod but I can't figure it out.

What's wrong with this call?

Update: In general what was wrong is that __init__ doesn't return an object.

Due to the hints and thoughts from the answers i modified my code to achieve what i need:

class Parent:

    def __init__(self, a):
        self.a = a

    @staticmethod
    def decorate_with_some_magic(func):
        def prepare_and_connect(*args, **kwargs):
            print("prepare something")
            print("create an object")
            obj = func(*args, **kwargs)
            print("connect obj to something")
            return obj

        return prepare_and_connect

    def __repr__(self):
        return f"{self.a}"


class ChildWithOneName(Parent):

    @classmethod
    @Parent.decorate_with_some_magic
    def prepare_and_connect_constructor(cls, a, b):
        """ use the generic connection decorator right on object creation """
        obj = super().__new__(cls)
        obj.__init__(a, b)
        print("Does the same as in it's __init__ method")
        return obj

    def __init__(self, a, b):
        """ init without connecting """
        super().__init__(a)
        self.b = b

    def __repr__(self):
        return f"{self.a}, {self.b}"


class GodChild(Parent):

    @classmethod
    @Parent.decorate_with_some_magic
    def prepare_and_connect_constructor(cls, a, names):
        """ use the generic connection decorator right on object creation """
        obj = super().__new__(cls)
        obj.__init__(a, names)
        # perform some more specific operations
        obj.register_all_names(names)
        print("And does some more stuff than in it's __init__ method")
        return obj

    def __init__(self, a, already_verified_names):
        """ init without connecting """
        super().__init__(a)
        self.verified_names = already_verified_names

    def register_all_names(self, names=[]):
        self.verified_names = []

        def verify(text):
            return True

        for name in names:
            if verify(name):
                self.verified_names.append(name)


    def __repr__(self):
        return f"{self.a}, {self.verified_names}"


if __name__ == '__main__':
    print(ChildWithOneName.prepare_and_connect_constructor("special child", "needs some help"), end='\n\n')
    print(GodChild.prepare_and_connect_constructor("unknown child", "needs some verification"), end='\n\n')
    print(ChildWithOneName("my child", "is clean and doesn't need extra magic"))

Upvotes: 1

Views: 530

Answers (2)

Mike Müller
Mike Müller

Reputation: 85432

Decorators work on callables. Since there is no difference between calling a function and initiating a class, you can use your decorator directly on the class:

def decorate_with_some_magic(func):
    def prepare_and_connect(*args, **kwargs):
        print("prepare something")
        print("create an object")
        obj = func(*args, **kwargs)
        print("connect obj to something")
        return obj

    return prepare_and_connect


class Parent:

    @classmethod
    def prepare_and_connect_constructor(cls, a, b):
        return decorate_with_some_magic(cls)(a, b)

    def __init__(self, a):
        self.a = a

    def __repr__(self):
        return f"{self.a}"

class Child(Parent):

    def __init__(self, a, b):
        """ init without connecting """
        super().__init__(a)
        self.b = b


    def __repr__(self):
        return f"{self.a}, {self.b}"


if __name__ == '__main__':
    normal_child = Child("normal child", "no help needed")
    print(normal_child)
    special_child = Child.prepare_and_connect_constructor("special child", "needs some help")
    print(special_child)

Output:

normal child, no help needed
prepare something
create an object
connect obj to something
special child, needs some help

Upvotes: 1

MegaIng
MegaIng

Reputation: 7886

You have a slight misunderstanding of the magic methods __init__ and __new__. __new__ creates a new object, e.g. returns a instance of the class. __init__ just modifies the object in place. So an easy fix for your problem would be de following:

@classmethod
@decorate_with_some_magic
def prepare_and_connect_constructor(cls, a, b):
    """ use the generic connection decorator right on object creation """
    obj = super().__new__(cls)
    obj.__init__(a)
    # put some more specific attributes (over the parents class)
    obj.b = b
    return obj

I however don't think you should use it like this. Instead, your probably should overwrite __new__

Upvotes: 1

Related Questions