Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are Python decorators with arguments syntactically different from those without?

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?
like image 684
kuzzooroo Avatar asked Nov 27 '13 05:11

kuzzooroo


1 Answers

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.

like image 137
Benno Avatar answered Oct 22 '22 09:10

Benno