How to decorate a class?

Viewed 150663

How do I create a decorator that applies to classes?

Specifically, I want to use a decorator addID to add a member __id to a class, and change the constructor __init__ to take an id argument for that member.

def getId(self): return self.__id

classdecorator addID(cls):
    def __init__(self, id, *args, **kws):
        self.__id = id
        self.getId = getId
        cls.__init__(self, *args, **kws)

@addID
class Foo:
    def __init__(self, value1):
        self.value1 = value1

The above should be equivalent to:

class Foo:
    def __init__(self, id, value1):
        self.__id = id
        self.value1 = value1

    def getId(self): return self.__id
8 Answers

Apart from the question whether class decorators are the right solution to your problem:

In Python 2.6 and higher, there are class decorators with the @-syntax, so you can write:

@addID
class Foo:
    pass

In older versions, you can do it another way:

class Foo:
    pass

Foo = addID(Foo)

Note however that this works the same as for function decorators, and that the decorator should return the new (or modified original) class, which is not what you're doing in the example. The addID decorator would look like this:

def addID(original_class):
    orig_init = original_class.__init__
    # Make copy of original __init__, so we can call it without recursion

    def __init__(self, id, *args, **kws):
        self.__id = id
        self.getId = getId
        orig_init(self, *args, **kws) # Call the original __init__

    original_class.__init__ = __init__ # Set the class' __init__ to the new one
    return original_class

You could then use the appropriate syntax for your Python version as described above.

But I agree with others that inheritance is better suited if you want to override __init__.

I would second the notion that you may wish to consider a subclass instead of the approach you've outlined. However, not knowing your specific scenario, YMMV :-)

What you're thinking of is a metaclass. The __new__ function in a metaclass is passed the full proposed definition of the class, which it can then rewrite before the class is created. You can, at that time, sub out the constructor for a new one.

Example:

def substitute_init(self, id, *args, **kwargs):
    pass

class FooMeta(type):

    def __new__(cls, name, bases, attrs):
        attrs['__init__'] = substitute_init
        return super(FooMeta, cls).__new__(cls, name, bases, attrs)

class Foo(object):

    __metaclass__ = FooMeta

    def __init__(self, value1):
        pass

Replacing the constructor is perhaps a bit dramatic, but the language does provide support for this kind of deep introspection and dynamic modification.

That's not a good practice and there is no mechanism to do that because of that. The right way to accomplish what you want is inheritance.

Take a look into the class documentation.

A little example:

class Employee(object):

    def __init__(self, age, sex, siblings=0):
        self.age = age
        self.sex = sex    
        self.siblings = siblings

    def born_on(self):    
        today = datetime.date.today()

        return today - datetime.timedelta(days=self.age*365)


class Boss(Employee):    
    def __init__(self, age, sex, siblings=0, bonus=0):
        self.bonus = bonus
        Employee.__init__(self, age, sex, siblings)

This way Boss has everything Employee has, with also his own __init__ method and own members.

Here is an example which answers the question of returning the parameters of a class. Moreover, it still respects the chain of inheritance, i.e. only the parameters of the class itself are returned. The function get_params is added as a simple example, but other functionalities can be added thanks to the inspect module.

import inspect 

class Parent:
    @classmethod
    def get_params(my_class):
        return list(inspect.signature(my_class).parameters.keys())

class OtherParent:
    def __init__(self, a, b, c):
        pass

class Child(Parent, OtherParent):
    def __init__(self, x, y, z):
        pass

print(Child.get_params())
>>['x', 'y', 'z']

Related