AdjunctProfessorFalcon
AdjunctProfessorFalcon

Reputation: 1840

Understanding the role of closure in decorators and function wrapper arguments

I'm trying to understand in the following code, how it is that arguments passed to the decorated function seem to be passed to the argument in the wrapper function:

def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

def p_decorate(func):
   def func_wrapper(*args, **kwargs):
       return "<p>{0}</p>".format(func(*args, **kwargs))
   return func_wrapper

my_get_text = p_decorate(get_text)

print my_get_text("John")

# <p>Outputs lorem ipsum, John dolor sit amet</p>

From trying to Google, I've gathered that the inner function func_wrapper() has access to variables or objects in the enclosing scope through closure (I think I'm understanding this correctly anyway).

What I'm not understanding is how, exactly, the value of the argument name passed to get_text(name) is accessed by (or given to or assigned to) *args or **kwargs in the inner function func_wrapper().

I think I'm correct in understanding that the entire get_text(name) function and its argument name is passed to p_decorate() and so is available in the scope within p_decorate() — but how are the arguments passed to func_wrapper() allowed to access the argument passed to get_text(name)? What is the process or methods involved that allow that to happen?

Upvotes: 1

Views: 1023

Answers (3)

BrenBarn
BrenBarn

Reputation: 251408

When you do my_get_text = p_decorate(get_text), you set my_get_text to the result of calling p_decorate(get_text). The result of calling p_decorate is your func_wrapper. So when you call my_get_text, you are effectively calling func_wrapper.

Then look at what func_wrapper does. It accepts any arguments (*args and *kwargs) and passes them to func. Since func was set to get_text when you called p_decorate, this calls get_text with the same arguments that my_get_text was called with.

You are right that there is a closure, but the closure doesn't really have anything to do with how the arguments in the call my_get_text("John") are passed. The role of the closure is to ensure that the value of func (namely, get_text) is "saved" in func_wrapper, so that the returned wrapper "knows" which function it is wrapping. But once the wrapped function is created, the actual argument-passing that occurs when you call it is just normal argument-passing. You call one function with arguments, and that function calls another function with the same arguments. It is no different than this:

def foo(x, y):
    return x+y

def bar(x, y):
    return foo(x, y)

If you now call bar, it calls foo. foo is called with the same arguments because bar calls it with the same arguments bar was called with. Likewise, in your example get_text gets the arguments because func_wrapper calls get_text with the same arguments func_wrapper was called with.

Upvotes: 3

Nikita
Nikita

Reputation: 6331

Two things to note:

  1. Variable defined in function defenition is defined in this functions local scope, which is an enclosing scope for any inner functions and therefore this variable is available in these inner functions. And function name (func in your code) is just variable referencing function object.

  2. *args syntax in function defenition says "collect all unmatched positional arguments as a tuple and give this tuple the name args", **kwargs says "collect all unmatched keyword arguments as a dictionary and give this dictionary the name kwargs". args and kwargs are just a convention (so as self in classes) and you can use any name in this place, though you shouldn't. In function call the same syntax * and ** does the opposite - it breaks a tuple and dictionary respectively in individual values and key=value pairs.

Now your code:

def get_text(name):
   #name is here in local scope and it's just a positional argument
   #if you've used get_text(*args, **kwargs) you could refer to 'name' as 'args[0]'
   return "lorem ipsum, {0} dolor sit amet".format(name)

def p_decorate(func):
   #'func' is saved here in local scope
   #here 'func' will reference 'get_text' from your example
   def func_wrapper(*args, **kwargs):
       #'args' and 'kwargs' are here in local scope and
       #they represent all arguments passed to decorated function
       #regarding your example here will be 'arg[0]="John"'
       return "<p>{0}</p>".format(func(*args, **kwargs))
       #in the line above in function you basicly break 'args' and 'kwargs'
       #into pieces and pass them to func as separate arguments
       #regarding your example you basicly call 'func("John")
       #where "John" is in 'arg[0]' and considering 'func' reference
       #in your example it's basicly 'get_text(name)'
   return func_wrapper
   #line above returns function object which will be assigned
   #to some name as result of decorator call, i.e. 'my_get_text'

my_get_text = p_decorate(get_text)
#effectively it says "set 'my_get_text' to reference 'func_wrapper'
#with argument 'func'='get_text'"

print my_get_text("John")
#effectively is says "call function referenced by 'my_get_text'
#which is 'func_wrapper' with argument 'John'  is equal
#to call 'func_wrapper("John")'"

# <p>Outputs lorem ipsum, John dolor sit amet</p>

Using *args and **kwargs makes decorator more universal. In your example, if you know that only one argument is used you could write something like:

def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

def p_decorate(func):
   def func_wrapper(single_argument):
       return "<p>{0}</p>".format(func(single_argument))
   return func_wrapper

my_get_text = p_decorate(get_text)

print my_get_text("John")

# <p>Outputs lorem ipsum, John dolor sit amet</p>

Hope this will make it clearer to understand.

And regardin decorator syntax mentioned in comments to your question, usin @ is just syntactic sugar so:

def p_devorate(func):
    ...

@p_decorate
def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

is the same as:

def p_devorate(func):
    ...

def get_text(name):
   return "lorem ipsum, {0} dolor sit amet".format(name)

get_text = p_decorate(get_text) 
#redefining the name of original function to pointed to wrappe function instead 

Upvotes: 1

Lav
Lav

Reputation: 2274

When you call...

my_get_text = p_decorate(get_text)

...function p_decorate() is executed with func = get_text. It defines a new function func_wrapper() which thus has access to func as it was set at the time of function definition.

So, the return value of p_decorate() is a newly minted function with signature of func_wrapper(*args, **kwargs) which also conveniently has access to func variable. Note that calling p_decorate() again will create a different func_wrapper() function with a different func variable.

Now, when you call this newly created function:

my_get_text("John")

You're essentially calling the equivalent of:

def func_wrapper(*args, **kwargs):
    func = get_text
    # ...

Sunce you're calling it with a single positional argument, it is equivalent to args = ("John",), kwargs = {}.

Upvotes: 1

Related Questions