Python Decorators
My notes on decorator functions (I don’t use classes enough to worry about class decorators).
Intro
- Decorators are functions designed to wrap other functions to enhance their capability at runtime.
- They do this by replacing the wrapped function with the return value of the decorator.
- They work as syntactic sugar for
decorated = decorator(decorated)
. - Decorators are run when the decorated function is defined, not when it is run (i.e. they run at import time, not runtime).
Basic mechanics
|
|
Running decorator
'Hello'
The above is equivalent to:
|
|
Running decorator
'Hello'
A decorator simply replaces the value of the function it wraps with the decorator’s return value, which can, in principle, be anything.
|
|
'Decorator return value'
Registration decorators
The simplest kind of decorator performs some kind of action and returns the function itself.
|
|
['greeter']
Notes:
greeter = register(greeter)
assignsgreeter
to itself, as that’s what’s returned byregister
.
Decorators that return a different function
|
|
[0.000000s] factorial(1) -> 1
[0.000353s] factorial(2) -> 2
[0.000719s] factorial(3) -> 6
6
Q&A:
What’s
functools.wraps()
about? It ensures that function metainformation is correctly handeled. For instance, thatfactorial.__name__
returns ‘factorial’. Without wraps, it would return ‘wrapper’.How does this work? By running
factorial = timer(factorial)
, the decorator assignsfactorial
towrapper
. Thus, when we callfactorial
we really callwrapper
, which returns the same resultfactorial
would have, but also performs the extra functionality. We can check the name attribute of factorial to confirm this; the decoratedfactorial
function points towrapper
, no longer tofactorial
. (In practice, we should decorate the wrapper function withfunctools.wraps(func)
to ensure that function meta information is passed through, so thatfactorial.__name__
would return ‘factorial’.)
|
|
'wrapper'
- How does
wrapper
have access tofunc
without taking it as an argument?func
is a variable of the local scope of thetimer
function, which makeswrapper
a closure: a function with access to variables that are neither global nor defined in its function body (my notes on closures). The below confirms this.
|
|
<function __main__.factorial(n)>
Where does
wrapper
get the arguments fromfactorial
from? The short answer is: the arguments are passed directly to it when we call the decoratedfactorial
function. This follows directly from the answer to the first question above: oncefactorial
is decorated, calling it actually callswrapper
.Why don’t we pass the function arguments as arguments to
timer
(i.e. why isn’t ittimer(func, *args)
? Because all timer does is replacefactorial
withwrapper
, which then gets called aswrapper(*args)
. So,timer
has no use for arguments.
Decorators with state
|
|
Call #1 of greeter
Hello
Call #2 of greeter
Hello
Call #1 of singer
lalala
Call #1 of congratulator
Congratulations!
Decorator with arguments
Now I want the ability to deactivate the logger for certain functions. So I wrap the decorator in a decorator factory, like so:
|
|
Call #1 of greeter
Hello
Call #2 of greeter
Hello
Call #1 of singer
lalala
Congratulations!
===== work in progress =====
How does this work? I’m not completely confident, actually, but this is how I explain it to myself.
How I think this works (not sure about this):
- temp = param_logger(), returns
decorator
with access to nonlocalactive
argument. - Because we add () to decorator,
decorator
is immediately called and returns wrapper, which is also assigned totemp
, i.e.temp = decorator(func) = wrapper(*args, **kwargs)
.
In our initial logger function above, both the argument to the outer function (func) and the variable defined inside the outer function (calls) are free variables of the closure function wrapper, meaning that wrapper has access to them even though they are not bound inside wrapper.
===== work in progress =====
If we remember that
|
|
is equivalent to
|
|
and if we know that we can use __code__.co_freevars
to get the free variables of a function, then it follows that we can get a view of the free variables of the decorated greeter function like so:
|
|
('calls', 'func')
This is as expected. Now, what are the free variables of param_logger?
|
|
('active',)
This makes sense: active is the function argument and we do not define any additional variables inside the scope of param_logger, so given our result above, this is what we would expect.
But param_logger is a decorator factory and not a decorator, which means it produces a decorator at the time of decoration. So, what are the free variables of the decorator it produces?
Similar to above, remembering that
|
|
is equivalent to
|
|
we can inspect the decorated greeter function’s free variables like so:
|
|
('active', 'calls', 'func')
We can see that active is now an additional free variable that our wrapper function has access to, which provides us with the answer to our question: decorator factories work by producing decorators at decoration time and passing on the specified keyword to the decorated function.
Decorator factory beautifying
A final point for those into aesthetics or coding consistency: we can tweak our decorator factory so that we can ommit the ()
if we pass no keyword arguments.
|
|
Call #1 of greeter
Hello
Call #2 of greeter
Hello
Call #1 of babler
bablebalbe
Call #1 of singer
lalala
Congratulations!
To understand what happens here, remember that decorating func with a decorator is equivalent to
|
|
While decorating it with a decorator factory is equivalent to
|
|
The control flow in the final return statement of the above decorator factory simply switches between these two cases: if logger gets a function argument, then that’s akin to the first scenario, where the func argument is passed into decorator directly, and so the decorator factory returns decorator(func) to mimic this behaviour. If func is not passed, then we’re in the standard decorator factory scenario above, and we simply return the decorator uncalled, just as any plain decorator factory would.
Recipe 9.6 in the Python Cookbook discusses a neat solution to the above for a registration decorator using functools.partial(), which I haven’t managed to adapt to a scenario with a decorator factory. Might give it another go later.
Mistakes I often make
I often do the below:
|
|
AttributeError: 'str' object has no attribute '__module__'
What’s wrong, there? @wraps
should be @wraps(func)
.
|
|
Func is called: greeter
'Hello World'
Applications
Reverse function arguments
|
|
Pass kwargs to decorator and make factory return function result
|
|
THIS IS VERY COOL!
Create tuple and supply kwargs upon function call in make_data.py
|
|
HA@
Can I just alter the parametrisation of func inside the factory based on the kwargs and then return the newly parametrised function without having to call it?