Search code examples
pythonpython-3.xfunctionpython-decorators

How arguments in Python decorated functions work


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>

Solution

  • 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.