3

I have troubles understanding how the argument is passed to a wrapper function inside a decorator. Take a simple example:

def my_decorator(func):
    def wrapper(func_arg):
        print('Before')
        func(func_arg)
        print('After')
    return wrapper

@my_decorator
def my_function(arg):
    print(arg + 1)

my_function(1)

I have a function that takes 1 argument and it is decorated. I have troubles in understanding how func_arg works. When my_function(1) is called, how is the value 1 passed to the wrapper. From my little understanding of this, is that my_function is 'replaced' by a new function like: my_function = my_decorator(my_function).

print(my_function)
<function my_decorator.<locals>.wrapper at 0x7f72fea9c620>
Arkon-v
  • 126
  • 1
  • 8
  • Your intuition is correct. `my_decorator` returns the `wrapper` function. The `@` syntax unrolls to the following call: `my_function = my_decorator(my_function)`. So it in fact replaces the function pointer you originally defined with the one it builds in place of its call. – Piotr Trochim Jan 28 '18 at 14:00

2 Answers2

6

Your understanding is entirely correct. Decorator syntax is just syntactic sugar, the lines:

@my_decorator
def my_function(arg):
    print(arg + 1)

are executed as

def my_function(arg):
    print(arg + 1)

my_function = my_decorator(my_function)

without my_function actually having been set before the decorator is called*.

So my_function is now bound to the wrapper() function created in your my_decorator() function. The original function object was passed into my_decorator() as the func argument, so is still available to the wrapper() function, as a closure. So calling func() calls the original function object.

So when you call the decorated my_function(1) object, you really call wrapper(1). This function receives the 1 via the name func_arg, and wrapper() then itself calls func(func_arg), which is the original function object. So in the end, the original function is passed 1 too.

You can see this result in the interpreter:

>>> def my_decorator(func):
...     def wrapper(func_arg):
...         print('Before')
...         func(func_arg)
...         print('After')
...     return wrapper
...
>>> @my_decorator
... def my_function(arg):
...     print(arg + 1)
...
>>> my_function
<function my_decorator.<locals>.wrapper at 0x10f278ea0>
>>> my_function.__closure__
(<cell at 0x10ecdf498: function object at 0x10ece9730>,)
>>> my_function.__closure__[0].cell_contents
<function my_function at 0x10ece9730>
>>> my_function.__closure__[0].cell_contents(1)
2

Closures are accessible via the __closure__ attribute, and you can access the current value for a closure via the cell_contents attribute. Here, that's the original decorated function object.

It is important to note that each time you call my_decorator(), a new function object is created. They are all named wrapper() but they are separate objects, each with their own __closure__.


* Python produces bytecode that creates the function object without assigning it to a name; it lives on the stack instead. The next bytecode instruction then calls the decorator object:

>>> import dis
>>> dis.dis(compile('@my_decorator\ndef my_function(arg):\n    print(arg + 1)\n', '', 'exec'))
  1           0 LOAD_NAME                0 (my_decorator)
              2 LOAD_CONST               0 (<code object my_function at 0x10f25bb70, file "", line 1>)
              4 LOAD_CONST               1 ('my_function')
              6 MAKE_FUNCTION            0
              8 CALL_FUNCTION            1
             10 STORE_NAME               1 (my_function)
             12 LOAD_CONST               2 (None)
             14 RETURN_VALUE

So first LOAD_NAME looks up the my_decorator name. Next, the bytecode generated for the function object is loaded as well as the name for the function. MAKE_FUNCTION creates the function object from those two pieces of information (removing them from the stack) and puts the resulting function object back on. CALL_FUNCTION then takes the one argument on the stack (it's operand 1 tells it how many positional arguments to take), and calls the next object on the stack (the decorator object loaded). The result of that call is then stored under the name my_function.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
2

Python decorator is a function that takes another function as an argument, generates a new function.

def my_decorator(func): 
   def wrapper(func_arg):
       print('Before') 
       func(func_arg) 
       print('After') 
       return wrapper
@my_decorator 
 def my_function(arg): 
    print(arg + 1)

So this is in effect same as:

my_function = my_decorator(my_function)

This means that return value of this decorator replaces the original function definition.

Just adding few points to the other answer. You can decorate your my_function by any number of other functions (stacking of decorators). When we stack multiple decorators, the order of execution is from the innermost to outermost.

For example:

@i_get_called_last
@i_get_called_second 
@i_get_called_first 
def my_function(arg): 
   print(arg + 1)

Here, you need to make sure that the first and second called decorators return compatible functions that the decorators preceding it would be able to operate with.

You may go through this to understand more.

Austin
  • 25,759
  • 4
  • 25
  • 48