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.
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
:
Seems entirely reasonable to me! But does the bytecode match that?
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:
decorator
gets pushed on to the stack.- The stack is
[decorator]
- The (combined) namespace is
{"decorator": decorator}
- The stack is
- The code object for
func
gets pushed on to the stack.[decorator, code-object-of-func]
{"decorator": decorator}
- A function object is created from the code object.
[decorator, function-object-of-func]
{"decorator": decorator}
decorator
is called.[return-value-of-decorator]
{"decorator": decorator}
func
gets assigned the value thatdecorator
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:
- Create
func
. - Give it a temporary name that isn't referenced anywhere.
- Delete the
func
name. - Apply all the decorators.
- Assign the resulting object to
func
.
This results in the code unravelling to: