174

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
Robert Siemer
  • 32,405
  • 11
  • 84
  • 94
Robert Gowland
  • 7,677
  • 6
  • 40
  • 58

8 Answers8

237

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__.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Steven
  • 28,002
  • 5
  • 61
  • 51
  • 1
    I'm not too deep into the rabbit hole of Python's magic methods, but woudn't defining dunder methods make even a function act like a(n old style) class when the needed methods are defined? If that is not what you want, don't name the methods that way but use `original_class.__init__ = arbitrary_method_name`. – hajef Apr 26 '19 at 09:44
  • @hajef no it would not. You could have tried it yourself. A function definition in a function does not make the latter into an attribute of the former (in a class, it would). And functions do have attributes, so that is not the problem.—A function def in a function is like setting a local variable to a lambda object, i.e. while executing the outer function, after passing the definition of the inner function, it is available until the end of the (outer) function. Nothing more. – Robert Siemer Feb 05 '23 at 00:15
87

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.

orokusaki
  • 55,146
  • 59
  • 179
  • 257
Jarret Hardie
  • 95,172
  • 10
  • 132
  • 126
  • 1
    Thank-you, that's what I'm looking for. A class that can modify any number of other classes such that they all have a particular member. My reasons for not having the classes inherit from a common ID class is that I want to have non-ID versions of the classes as well as ID versions. – Robert Gowland Mar 25 '09 at 15:51
  • 3
    Metaclasses used to be the go-to way to do things like this in Python2.5 or older, but nowadays you can very often use class decorators (see Steven's answer), which are much simpler. – Jonathan Hartley Apr 06 '16 at 21:35
34

No one has explained that you can dynamically define classes. So you can have a decorator that defines (and returns) a subclass:

def addId(cls):

    class AddId(cls):

        def __init__(self, id, *args, **kargs):
            super(AddId, self).__init__(*args, **kargs)
            self.__id = id

        def getId(self):
            return self.__id

    return AddId

Which can be used in Python 2 (the comment from Blckknght which explains why you should continue to do this in 2.6+) like this:

class Foo:
    pass

FooId = addId(Foo)

And in Python 3 like this (but be careful to use super() in your classes):

@addId
class Foo:
    pass

So you can have your cake and eat it - inheritance and decorators!

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
andrew cooke
  • 45,717
  • 10
  • 93
  • 143
  • 5
    Subclassing in a decorator is dangerous in Python 2.6+, because it breaks `super` calls in the original class. If `Foo` had a method named `foo` that called `super(Foo, self).foo()` it would recurse infinitely, because the name `Foo` is bound to the subclass returned by the decorator, not the original class (which is not accessible by any name). Python 3's argumentless `super()` avoids this issue (I assume via the same compiler magic that allows it to work at all). You can also work around the issue by manually decorating the class under different name (as you did in the Python 2.5 example). – Blckknght Sep 21 '13 at 22:02
  • 1
    huh. thanks, i had no idea (i use python 3). will add a comment. – andrew cooke Sep 21 '13 at 22:03
  • @Blckknght @andrew If we are using `super()`, then it should be safe, right? – Shiv Krishna Jaiswal May 22 '22 at 13:59
  • but then `Foo.__name__ == 'AddId'` .. any idea how to avoid that? – Alois Mahdal Jun 06 '23 at 21:07
13

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.

Esteban Küber
  • 36,388
  • 15
  • 79
  • 97
mpeterson
  • 741
  • 1
  • 7
  • 17
  • 4
    I guess what I wanted to was have Boss agnostic of the class that it contains. That is, there may be dozens of different classes that I want to apply Boss features to. Am I left with having these dozen classes inherit from Boss? – Robert Gowland Mar 25 '09 at 15:40
  • 5
    @Robert Gowland: That's why Python has multiple inheritance. Yes, you should inherit various aspects from various parent classes. – S.Lott Mar 25 '09 at 17:16
  • 7
    @S.Lott: In general multiple inheritance is a bad idea, even too much levels of inheritance is bad too. I shall recommend you to stay away from multiple inheritance. – mpeterson Mar 26 '09 at 12:39
  • 6
    mpeterson: Is multiple inheritance in python worse than this approach? What's wrong with python's multiple inheritance? – Arafangion Aug 04 '09 at 02:00
  • 2
    @Arafangion: Multiple inheritance is generally considered to be a warning sign of trouble. It makes for complex hierarchies and hard-to-follow relationships. If your problem domain lends itself well to multi inheritance, (can it be modelled hierarchycally?) it is a natural choice for many. This applies to all languages that allow multi-inheritance. – Morten Jensen Sep 19 '12 at 06:48
11

I'd agree inheritance is a better fit for the problem posed.

I found this question really handy though on decorating classes, thanks all.

Here's another couple of examples, based on other answers, including how inheritance affects things in Python 2.7, (and @wraps, which maintains the original function's docstring, etc.):

def dec(klass):
    old_foo = klass.foo
    @wraps(klass.foo)
    def decorated_foo(self, *args ,**kwargs):
        print('@decorator pre %s' % msg)
        old_foo(self, *args, **kwargs)
        print('@decorator post %s' % msg)
    klass.foo = decorated_foo
    return klass

@dec  # No parentheses
class Foo...

Often you want to add parameters to your decorator:

from functools import wraps

def dec(msg='default'):
    def decorator(klass):
        old_foo = klass.foo
        @wraps(klass.foo)
        def decorated_foo(self, *args ,**kwargs):
            print('@decorator pre %s' % msg)
            old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)
        klass.foo = decorated_foo
        return klass
    return decorator

@dec('foo decorator')  # You must add parentheses now, even if they're empty
class Foo(object):
    def foo(self, *args, **kwargs):
        print('foo.foo()')

@dec('subfoo decorator')
class SubFoo(Foo):
    def foo(self, *args, **kwargs):
        print('subfoo.foo() pre')
        super(SubFoo, self).foo(*args, **kwargs)
        print('subfoo.foo() post')

@dec('subsubfoo decorator')
class SubSubFoo(SubFoo):
    def foo(self, *args, **kwargs):
        print('subsubfoo.foo() pre')
        super(SubSubFoo, self).foo(*args, **kwargs)
        print('subsubfoo.foo() post')

SubSubFoo().foo()

Outputs:

@decorator pre subsubfoo decorator
subsubfoo.foo() pre
@decorator pre subfoo decorator
subfoo.foo() pre
@decorator pre foo decorator
foo.foo()
@decorator post foo decorator
subfoo.foo() post
@decorator post subfoo decorator
subsubfoo.foo() post
@decorator post subsubfoo decorator

I've used a function decorator, as I find them more concise. Here's a class to decorate a class:

class Dec(object):

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

    def __call__(self, klass):
        old_foo = klass.foo
        msg = self.msg
        def decorated_foo(self, *args, **kwargs):
            print('@decorator pre %s' % msg)
            old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)
        klass.foo = decorated_foo
        return klass

A more robust version that checks for those parentheses, and works if the methods don't exist on the decorated class:

from inspect import isclass

def decorate_if(condition, decorator):
    return decorator if condition else lambda x: x

def dec(msg):
    # Only use if your decorator's first parameter is never a class
    assert not isclass(msg)

    def decorator(klass):
        old_foo = getattr(klass, 'foo', None)

        @decorate_if(old_foo, wraps(klass.foo))
        def decorated_foo(self, *args ,**kwargs):
            print('@decorator pre %s' % msg)
            if callable(old_foo):
                old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)

        klass.foo = decorated_foo
        return klass

    return decorator

The assert checks that the decorator has not been used without parentheses. If it has, then the class being decorated is passed to the msg parameter of the decorator, which raises an AssertionError.

@decorate_if only applies the decorator if condition evaluates to True.

The getattr, callable test, and @decorate_if are used so that the decorator doesn't break if the foo() method doesn't exist on the class being decorated.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Chris
  • 5,664
  • 6
  • 44
  • 55
6

There's actually a pretty good implementation of a class decorator here:

https://github.com/agiliq/Django-parsley/blob/master/parsley/decorators.py

I actually think this is a pretty interesting implementation. Because it subclasses the class it decorates, it will behave exactly like this class in things like isinstance checks.

It has an added benefit: it's not uncommon for the __init__ statement in a custom django Form to make modifications or additions to self.fields so it's better for changes to self.fields to happen after all of __init__ has run for the class in question.

Very clever.

However, in your class you actually want the decoration to alter the constructor, which I don't think is a good use case for a class decorator.

Jordan Reiter
  • 20,467
  • 11
  • 95
  • 161
  • I don't see any subclassing happening, do you mean `parsleyfy`? It is wrapping the init method. Interesting indeed. – seb May 02 '22 at 03:41
1

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']

0

Django has method_decorator which is a decorator that turns any decorator into a method decorator, you can see how it's implemented in django.utils.decorators:

https://github.com/django/django/blob/50cf183d219face91822c75fa0a15fe2fe3cb32d/django/utils/decorators.py#L53

https://docs.djangoproject.com/en/3.0/topics/class-based-views/intro/#decorating-the-class

Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103