Unravelling decorators

For the next post in my syntactic sugar series, I thought I would tackle decorators.

Let's look at a simple example of a function that has a single decorator applied to it.

def func():
Decorator example

If you're familiar with decorators then your mental model for this code may be that the code passes func to decorator and assigns the return value back to func:

def func():
func = decorator(func)
Reasonable mental model for the unravelling of the decorator example

Seems entirely reasonable to me! But does the bytecode match that?

  2           0 LOAD_GLOBAL              0 (decorator)

  3           2 LOAD_CONST               1 (<code object func at 0x100764240, file "<stdin>", line 2>)
              4 LOAD_CONST               2 ('spam.<locals>.func')
              6 MAKE_FUNCTION            0
              8 CALL_FUNCTION            1
             10 STORE_FAST               0 (func)
Bytecode for the decorator example

It turns out there's a slight "cheat" that the bytecode gets to take which we didn't use in our mental model. Here's what the bytecode is doing:

  1. decorator gets pushed on to the stack.
    • The stack is [decorator]
    • The (combined) namespace is {"decorator": decorator}
  2. The code object for func gets pushed on to the stack.
    • [decorator, code-object-of-func]
    • {"decorator": decorator}
  3. A function object is created from the code object.
    • [decorator, function-object-of-func]
    • {"decorator": decorator}
  4. decorator is called.
    • [return-value-of-decorator]
    • {"decorator": decorator}
  5. func gets assigned the value that decorator returned.
    • []
    • {"decorator": decorator, "func": return-value-of-decorator}

Notice how not until the very end is anything actually assigned to the name func? Because the execution stack can hold values without them being assigned to a name, Python is able to avoid assigning the final value to func until the very end after all decorators are evaluated. This is specifically mentioned in the language definition:

... the original function is not temporarily bound to the name func.

My assumption is this is to protect users from accidentally referring to the name before the final object that is to be func is created and available.

To replicate this we need to assign the function to a temporary name that is not referenced anywhere else to avoid the function's actual name from being exposed too early. You might think to initially define the function with a temporary name, but you want the function object to be accurate and a code object's co_name attribute is read-only and so we can't patch things up appropriately later. What all of this means is we need to:

  1. Create func.
  2. Give it a temporary name that isn't referenced anywhere.
  3. Delete the func name.
  4. Apply all the decorators.
  5. Assign the resulting object to func.

This results in the code unravelling to:

def func():

_temp_func_name = func
del func

func = decorator(_temp_func_name)
Unravelling of decorators