8

Why arn't the following two scripts equivalent?

(Taken from another question: Understanding Python Decorators)

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makebold
@makeitalic
def hello():
    return "hello world"

print hello() ## returns <b><i>hello world</i></b>

and with a decorated decorator:

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

@makebold
def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makeitalic
def hello():
    return "hello world"

print hello() ## TypeError: wrapped() takes no arguments (1 given)

Why do I want to know? I've written a retry decorator to catch MySQLdb exceptions - if the exception is transient (e.g. Timeout) it will re-call the function after sleeping a bit.

I've also got a modifies_db decorator which takes care of some cache-related housekeeping. modifies_db is decorated with retry, so I assumed that all functions decorated with modifies_db would also retry implicitly. Where did I go wrong?

Community
  • 1
  • 1
RobM
  • 8,373
  • 3
  • 45
  • 37
  • Good question. I ran into the same scenario a few months ago while doing my own retry decorator. It took a retry count, so the problem presented itself a little different than yours, but had the same solutions as seen below. – Ryann Graham Jan 19 '10 at 14:18

3 Answers3

9

The problem with the second example is that

@makebold
def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

is trying to decorate makeitalic, the decorator, and not wrapped, the function it returns.

You can do what I think you intend with something like this:

def makeitalic(fn):
    @makebold
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

Here makeitalic uses makebold to decorate wrapped.

Will Harris
  • 21,597
  • 12
  • 64
  • 64
  • 2
    +1 This exactly points out the problem and how it needs to be done instead. Basically the problem comes from thinking decorators do function composition - but they dont. – Jochen Ritzel Jan 19 '10 at 15:07
  • Yup that's exactly what I was trying to do, and yes I misunderstood that decorators == functional composition – RobM Jan 19 '10 at 15:23
1

The reason is because wrapped() inside of makebold doesn't accept any arguments.

When you use the decorator like that it can cause some issues, I'll post an example of how to achieve what you're wanting though, give me just a moment.

Here is a working example of what you need.

def makebold(rewrap=False):
    if rewrap:
        def inner(decorator):
            def rewrapper(func):
                def wrapped(*args, **kwargs):
                    return "<b>%s</b>" % decorator(func)(*args,**kwargs)
                return wrapped
            return rewrapper
        return inner

    else:
        def inner(func):
            def wrapped(*args, **kwargs):
                return "<b>%s</b>" % func(*args, **kwargs)    
            return wrapped
        return inner

@makebold(rewrap=True)
def makeitalic(fn):
    def wrapped(*args, **kwargs):
        return "<i>%s</i>" % fn(*args, **kwargs)
    return wrapped

@makeitalic
def hello():
    return "hello world"

@makebold()
def hello2():
    return "Bob Dole"    

if __name__ == "__main__":
    print hello()   
    print hello2()

makebold is kinda ugly, but it shows you how to write a decorator that can optionally wrap another decorator.

Here is the output from the above script:

<b><i>hello world</i></b>
<b>Bob Dole</b>

Note that makebold is the only recursive decorator. Also note the subtle difference in usage: @makebold() vs @makeitalic.

Bryan McLemore
  • 6,438
  • 1
  • 26
  • 30
  • -1... overly complex, and "makeitalic" is a bad name for something that does bold + italic. See [this answer](http://stackoverflow.com/a/739665/850830) which does the same thing in a few lines of code. – Ryan Jul 20 '14 at 01:47
0

The problem is replacing "makeitalic" (which takes one argument) with the "wrapped"-function in "makebold" which takes zero arguments.

Use *args, **kwargs to pass on arguments further down the chain:

def wrapped(*args, **kwargs):
    return "<b>" + fn(*args, **kwargs) + "</b>"
mthurlin
  • 26,247
  • 4
  • 39
  • 46