8

This article, linked to a number of times from various stackoverflow questions, describes how decorators with arguments are syntactically different from those without arguments.

  • Decorators without arguments: "Notice that __init__() is the only method called to perform decoration, and __call__() is called every time you call the decorated sayHello()."
  • Decorators with arguments: "Now the process of decoration calls the constructor and then immediately invokes __call__(), which can only take a single argument (the function object) and must return the decorated function object that replaces the original. Notice that __call__() is now only invoked once, during decoration, and after that the decorated function that you return from __call__() is used for the actual calls."

The explanation given in the article doesn't tell me why the language is set up this way:

Although this behavior makes sense -- the constructor is now used to capture the decorator arguments, but the object __call__() can no longer be used as the decorated function call, so you must instead use __call__() to perform the decoration -- it is nonetheless surprising the first time you see it

There are two related features of this setup that are uncomfortable for me:

  1. Why provide a way to make no-argument decorators that's not just a special case of the more general method for specifying decorators? And why use __init__ and __call__ in both, but have them mean different things?
  2. Why use __call__ in the case of decorators with arguments for a purpose other than calling the decorated function (as the name suggests, at least coming from the no argument case)? Given that __call__ is only invoked right after __init__, why not just pass the function to be decorated as an argument to __init__ and handle everything that would happen in __call__ in __init__ instead?
flonk
  • 3,726
  • 3
  • 24
  • 37
kuzzooroo
  • 6,788
  • 11
  • 46
  • 84

1 Answers1

9

It's because it's the decorator object that's being called in both cases. To make it clearer, given this:

def my_decorator(a):
    def wrapper(f):
        def wrapped_function():
            return f() + a
        return wrapped_function
    return wrapper

this:

@my_decorator(5)
def function():
    return 5

is equivalent to this:

decorator = my_decorator(5)

@decorator
def function():
    return 5

What's happening in the no-argument case is that the decorator gets invoked directly instead of having to return a function that takes the object to be decorated as a parameter.

Benno
  • 5,640
  • 2
  • 26
  • 31
  • 4
    In other words, `wrapper` is the decorator, `my_decorator` isn't a decorator, it's a function that _returns_ a decorator (`wrapper` in this case). However it feels natural enough to treat `my_decorator` as a "decorator with arguments" – John La Rooy Nov 27 '13 at 05:39
  • 1
    Could you check that your code runs? I get "TypeError: 'int' object is not callable" when I try to run the decorated function() under Python 2.7. – kuzzooroo Nov 27 '13 at 05:50
  • I see. The [Bruce Eckel article](http://www.artima.com/weblogs/viewpost.jsp?thread=240808) presents using classes as decoration mechanisms rather than functions, but when it comes to understanding the difference between the args and no-args cases, I think you're right to show functions instead. – kuzzooroo Nov 27 '13 at 06:09
  • The Pythonic term for what a decorator needs to be is a "callable". Classes are callable, in which case their `__init__` method is invoked. Objects are callable if they have a `__call__` method. This is what's happening in that article. Functions are, by their nature, callable. – Benno Nov 27 '13 at 06:11
  • BTW, this answer makes it clear why there's a no-argument version that's not a special case of the with-arguments version: the latter is built on top of the former. I don't see any reason why they had to use the '@' character for both, though. – kuzzooroo Nov 27 '13 at 16:20