Can I combine two decorators into a single one in Python?

PythonDecorator

Python Problem Overview


Is there a way to combine two decorators into one new decorator in python?

I realize I can just apply multiple decorators to a function, but I was curious as to whether there's some simple way to combine two into a new one.

Python Solutions


Solution 1 - Python

A bit more general:

def composed(*decs):
    def deco(f):
        for dec in reversed(decs):
            f = dec(f)
        return f
    return deco

Then

@composed(dec1, dec2)
def some(f):
    pass

is equivalent to

@dec1
@dec2
def some(f):
    pass

Solution 2 - Python

Yes. See the definition of a decorator, here.

Something like this should work:

def multiple_decorators(func):
   return decorator1(decorator2(func))

@multiple_decorators
def foo(): pass

Solution 3 - Python

Decorators are just functions that take a function as input and return a new function. This:

@deco
def foo():
    ...

Is equivalent to this:

def foo():
    ...

foo = deco(foo)

In other words, the decorated function (foo) is passed as an argument to the decorator, and then foo is replaced with the return value of the decorator. Equipped with this knowledge, it's easy to write a decorator that combines two other decorators:

def merged_decorator(func):
    return decorator2(decorator1(func))

# now both of these function definitions are equivalent:

@decorator2
@decorator1
def foo():
    ...

@merged_decorator
def foo():
    ...

It gets a little trickier if the decorators accept arguments, like these two:

@deco_with_args2(bar='bar')
@deco_with_args1('baz')
def foo():
    ...

You might wonder how these decorators are even implemented. It's actually pretty simple: deco_with_args1 and deco_with_args2 are functions that return another function decorator. Decorators with arguments are essentially decorator factories. The equivalent of this:

@deco_with_args('baz')
def foo():
    ...

Is this:

def foo():
    ...

real_decorator = deco_with_args('baz')
foo = real_decorator(foo)

In order to make a decorator that accepts arguments and then applies two other decorators, we have to implement our own decorator factory:

def merged_decorator_with_args(bar, baz):
    # pass the arguments to the decorator factories and
    # obtain the actual decorators
    deco2 = deco_with_args2(bar=bar)
    deco1 = deco_with_args1(baz)
    
    # create a function decorator that applies the two
    # decorators we just created
    def real_decorator(func):
        return deco2(deco1(func))
    
    return real_decorator

This decorator can then be used like this:

@merged_decorator_with_args('bar', 'baz')
def foo():
    ...

Solution 4 - Python

If the decorators don't take additional arguments, you could use

def compose(f, g):
    return lambda x: f(g(x))

combined_decorator = compose(decorator1, decorator2)

Now

@combined_decorator
def f():
    pass

will be equivalent to

@decorator1
@decorator2
def f():
    pass

Solution 5 - Python

If you don't want to repeat yourself too much in a test suite, you could do like this::

def apply_patches(func):
    @functools.wraps(func)
    @mock.patch('foo.settings.USE_FAKE_CONNECTION', False)
    @mock.patch('foo.settings.DATABASE_URI', 'li://foo')
    @mock.patch('foo.connection.api.Session.post', autospec=True)
    def _(*args, **kwargs):
        return func(*args, **kwargs)

    return _

now you can use that in your test suite instead of a crazy amount of decorators above each function::

def ChuckNorrisCase(unittest.TestCase):
    @apply_patches
    def test_chuck_pwns_none(self):
        self.assertTrue(None)

Solution 6 - Python

And to extend @Jochen's answer:

import click


def composed(*decs):
    def deco(f):
        for dec in reversed(decs):
            f = dec(f)
        return f
    return deco


def click_multi(func):
    return composed(
        click.option('--xxx', is_flag=True, help='Some X help'),
        click.option('--zzz', is_flag=True, help='Some Z help')
    )(func)


@click_multi
def some_command(**args):
    pass

In this example you can compose a new decorator that contains multiple decorators.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLudoView Question on Stackoverflow
Solution 1 - PythonJochen RitzelView Answer on Stackoverflow
Solution 2 - PythonThanatosView Answer on Stackoverflow
Solution 3 - PythonAran-FeyView Answer on Stackoverflow
Solution 4 - PythonSven MarnachView Answer on Stackoverflow
Solution 5 - PythonspecialunderwearView Answer on Stackoverflow
Solution 6 - PythonmraxusView Answer on Stackoverflow