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)assignsgreeterto 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 assignsfactorialtowrapper. Thus, when we callfactorialwe really callwrapper, which returns the same resultfactorialwould have, but also performs the extra functionality. We can check the name attribute of factorial to confirm this; the decoratedfactorialfunction 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
wrapperhave access tofuncwithout taking it as an argument?funcis a variable of the local scope of thetimerfunction, which makeswrappera 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
wrapperget the arguments fromfactorialfrom? The short answer is: the arguments are passed directly to it when we call the decoratedfactorialfunction. This follows directly from the answer to the first question above: oncefactorialis 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 replacefactorialwithwrapper, which then gets called aswrapper(*args). So,timerhas 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
decoratorwith access to nonlocalactiveargument. - Because we add () to decorator,
decoratoris 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?