glpsx
glpsx

Reputation: 679

Understanding how passing arguments to the decorated function works

I'm learning about Python decorators and I have some difficulties understanding how passing arguments to the decorated function works. I'm not talking about passing arguments to the decorator itself but of the case where the original function takes additional arguments. For instance, I'm trying to understand the following simple example (credits for the code snippet goes to this answer):

def decorator(fn):
    def wrapper(arg1, arg2):
        print("I got args! Look: {0}, {1}".format(arg1, arg2))
        fn(arg1, arg2)
    return wrapper

@decorator
def print_full_name(first_name, last_name):
    print("My name is {0} {1}".format(first_name, last_name))

print_full_name("John", "Doe")
# outputs:
# I got args! Look: John Doe
# My name is John Doe

So, if I intentionally break the code by removing the arguments arg1 and arg2 of the wrapper, I get the following error: TypeError: wrapper() takes 0 positional arguments but 2 were given. It's strange to me that the wrapper is expecting 2 arguments when it's defined without any arguments inside the parentheses. Also, is it unclear to me how the first_name and last_name argument values are "mapped" to the arguments of the wrapper function. We only pass those arguments to the print_full_name function but it seems they also get passed to the wrapper function. Maybe this has to do with the order in which things are run but this is not clear for me.

I know they are a lot of great answers here regarding this topic but I could not find one that clearly explains this specific part. Any help would be greatly appreciated.

Upvotes: 0

Views: 54

Answers (2)

Xxxo
Xxxo

Reputation: 1931

You should break down the calls that happen.

So, you have:

def decorator(fn):
    def wrapper(arg1, arg2):
        print("I got args! Look: {0}, {1}".format(arg1, arg2))
        fn(arg1, arg2)
    return wrapper

@decorator
def print_full_name(first_name, last_name):
    print("My name is {0} {1}".format(first_name, last_name))

print_full_name("John", "Doe")
# outputs:
# I got args! Look: John Doe
# My name is John Doe

Because you have decorated the print_full_name with the decorator, then when you do

print_full_name("John", "Doe")

first the decorator decorator is called with argument print_full_name, as in:

decorator(print_full_name)("John", "Doe")

The decorator(print_full_name) is defined as

def decorator(fn):
    def wrapper(...):
        ...
        ...
    return wrapper

and returns the wrapper. Thus, the above turns out to be:

wrapper("John", "Doe")

Then, because the wrapper is defined as

def wrapper(arg1, arg2)

in the body of the wrapper you have that arg1="John" and arg2="Doe".

Finally, because the wrapper is defined inside the decorator, it knows what the fn is, where fn=print_full_name.

And that is how it works.

Upvotes: 1

chepner
chepner

Reputation: 530970

In short: passing arguments is exactly the same; you just need to be aware of which function you are actually calling.


Decorator syntax is just function application in disguise. Without it, you would just write

def decorator(fn):
    def wrapper(arg1, arg2):
        print("I got args! Look: {0}, {1}".format(arg1, arg2))
        fn(arg1, arg2)
    return wrapper


def print_full_name(first_name, last_name):
    print("My name is {0} {1}".format(first_name, last_name))

print_full_name = decorator(print_full_name)

So now, print_full_name does not refer to the original function that prints the sentence. It refers to the function returned by decorator. The original function is not lost; the new function is a closure over the name fn, which is a reference to the original function.

Upvotes: 1

Related Questions