25

I'm trying to understand how to decorate decorators, and wanted to try out the following:

Let's say I have two decorators and apply them to the function hello():

def wrap(f):
    def wrapper():
        return " ".join(f())
    return wrapper


def upper(f):
    def uppercase(*args, **kargs):
        a,b = f(*args, **kargs)
        return a.upper(), b.upper()
    return uppercase

@wrap
@upper
def hello():
    return "hello","world"

print(hello())

Then I have to start adding other decorators for other functions, but in general the @wrap decorator will "wrap all of them"

def lower(f):
    def lowercase(*args, **kargs):
        a,b = f(*args, **kargs)
        return a.lower(), b.lower()
    return lowercase

@wrap
@lower
def byebye():
    return "bye", "bye"

How do I write a decorator, which decorates my @lower and @upper decorators? See below:

@wrap
def lower():
    ...

@wrap
def upper():
    ...

To achieve the same result as above by only doing:

@upper
def hello():
    ...

@lower
def byebye():
    ...
Mike Pennington
  • 40,496
  • 17
  • 132
  • 170
ashwoods
  • 2,139
  • 1
  • 21
  • 36
  • 1
    You probably want to read this: http://stackoverflow.com/questions/739654/understanding-python-decorators. There is an example of decorating decorators at the end, but the answer introduce you slowly to it. – e-satis Jun 18 '11 at 09:03

2 Answers2

31
def upper(f):
    @wrap
    def uppercase(*args, **kargs):
        a,b = f(*args, **kargs)
        return a.upper(), b.upper()
    return uppercase

A decorator in Python

 @foo
 def bar(...): ...

is just equivalent to

 def bar(...): ...
 bar = foo(bar)

You want to get the effect of

@wrap
@upper
def hello(): ....

i.e.

hello = wrap(upper(hello))

so the wrap should be called on the return value of upper:

def upper_with_wrap(f):
   def uppercase(...): ...
   return wrap(uppercase)

which is also equivalent to applying the decorator on that function:

def upper_with_wrap(f):
   @wrap
   def uppercase(...): ...
   # ^ equivalent to 'uppercase = wrap(uppercase)'
   return uppercase
kennytm
  • 491,404
  • 99
  • 1,053
  • 989
  • 1
    this is a nice solution, but isn't exactly what I was asking, although maybe I should have stated it more clearly in the question. Although using this technique is cleaner (and easier to understand), I was looking into how to explicitly decorate a decorator. You get my upvote though, wish I could give more, specially for the time taken in explaining your answer :) – ashwoods May 11 '11 at 10:00
  • Exactly what I was looking for! – Olshansk Nov 15 '19 at 16:02
  • Note that `@wrap` here has nothing to do with the python builtin `@wraps`! – YPCrumble Mar 10 '22 at 18:43
12

Here's a generic (and slightly convoluted) solution for decorating decorators with decorators (Yay!).

# A second-order decorator
def decdec(inner_dec):
    def ddmain(outer_dec):
        def decwrapper(f):
            wrapped = inner_dec(outer_dec(f))
            def fwrapper(*args, **kwargs):
               return wrapped(*args, **kwargs)
            return fwrapper
        return decwrapper
    return ddmain

def wrap(f):
    def wrapper():
        return " ".join(f())
    return wrapper


# Decorate upper (a decorator) with wrap (another decorator)
@decdec(wrap)
def upper(f):
    def uppercase(*args, **kargs):
        a,b = f(*args, **kargs)
        return a.upper(), b.upper()
    return uppercase

@upper
def hello():
    return "hello","world"

print(hello())
Mike Pennington
  • 40,496
  • 17
  • 132
  • 170
Boaz Yaniv
  • 6,136
  • 20
  • 29
  • This answers the question, almost. You are decorating a decorator, but I wasn't looking for a generic solution, I actually wanted a more simple decorated decorator as somehow my head has to get around the arguments/assignments, no matter how often i go over examples :) – ashwoods May 11 '11 at 10:00
  • Well, you can make it non-generic of course. It's even quite simple: write `mywrap = decdec(wrap)` and then use @mywrap as a decorator for decorators. – Boaz Yaniv May 11 '11 at 10:57