Search code examples
pythonpython-decorators

Decorator with Arguments: Would this be a better way?


I just started learning decorators. I feel I understand how to modify a decorator to use arguments, however I don't see why the decorator in Python was implemented so that this modification is necessary.

[Edit: For clarity.] For a decorator without arguments, the below are equivalent:

@decorate
def function(arg):
    pass

function = decorate(function)

So, for a decorator with arguments, why not let the below be equivalent?

@decorate(123)
def function(arg):
    pass

function = decorate(function, 123)

I find this more consistent, easier to read, and easier to code.

Below is how python decorators work followed by how I expected them to work. [Edit: I added the expected output as well.]:

OUTPUT:
function HELLA NESTED! Decorated with OMG!
function NOT AS NESTED! Decorated with NOT SO BAD!

def python_decorator(dec_arg):
    def actual_decorator(func):
        def decorated_func(func_arg):
            print(func.__name__, func(func_arg), "Decorated with", dec_arg)

        return decorated_func
    return actual_decorator

@python_decorator("OMG!")
def function(arg):
    return arg

function("HELLA NESTED!")

def my_expected_decorator(func, dec_arg):
    # I expected the `func` parameter to be like `self` in classes---it's 
    # always first and is given special meaning with decorators.
    def decorated_func(func_arg):
        print(func.__name__, func(func_arg), "Decorated with", dec_arg)

    return decorated_func

# @my_expected_decorator("NOT SO BAD!") # This is what I expected.
def function(arg):
    return arg
function = my_expected_decorator(function, "NOT SO BAD!")

function("NOT AS NESTED!")

I looked at PEP 318 and didn't see a reason why decorators were not implemented in the above way, but again I'm new to this idea. I feel the above change would make decorators easier and more consistent to use.


Solution

  • The reason why the current implementation was chosen is because it's more consistent. If it was implemented the way you suggested, python would have to handle decorators very differently depending on how they're used:

    • If the decorator takes no additional arguments like @python_decorator, it can be called immediately with the function as the sole argument.
    • If the decorator is called with arguments like @python_decorator("OMG!"), python has to store those arguments somewhere and then insert the function as the first argument.
    • If the decorator is called without arguments like @python_decorator(), what should happen? Should this be equivalent to @python_decorator? But then why are there two different ways to use a decorator with no arguments? Maybe that syntax shouldn't be allowed?

    As you can see, an implementation like this would add some inconsistencies. The chosen implementation on the other hand is really quite intuitive: The code after the @ symbol can be thought of as an expression, and the result of that expression is used as the decorator:

    • In the case of @python_decorator, python_decorator is an expression that simply returns an existing decorator function.
    • In the case of @python_decorator("OMG!"), the expression python_decorator("OMG!") also returns a decorator function.

    In this sense, the chosen implementation is much more consistent than the one you proposed.

    This rationale is also explained in the PEP:

    The rationale for having a function that returns a decorator is that the part after the @ sign can be considered to be an expression (though syntactically restricted to just a function), and whatever that expression returns is called. See declaration arguments [16].


    Regarding the suggestions voiced in the comments:

    • Banning the syntax @python_decorator syntax and enforcing @python_decorator() instead

      The problem I see with this is that the parentheses serve no real purpose and only add clutter. The vast majority of decorators is applied without arguments. It's unreasonable to change the syntax for this just to be consistent with decorators that take arguments.

    • Explicitly passing the decorated function as the first argument

      Again, this wouldn't add any semantic meaning. It's already obvious that the decorator will be called on the function that's defined directly below the @python_decorator line. Forcing to user the explicitly pass the function as the argument is bad because

      1. It's a source of errors. It doesn't really add anything to your code, but a simple typo can cause an error. If you want to rename a decorated function, you now have to rename it in two places instead of just one.
      2. The decorated function doesn't even exist yet! Since the function definition comes after the decorator, it's impossible to refer to the function by name in the decorator.
      3. The whole point of decorators was to simplify and improve the func = decorator(func) syntax that had to be used before decorators existed. Having to write @decorator(func) instead wouldn't really be a significant improvement, would it?