Quite some time ago, I’d posted an entry about Python Class decorators; recently I happened to butt heads (again) against decorators, so I thought I’d have another go at documenting what goes where.
As usual, I recommend this post as a very good summary, and I won’t repeat what is already there.
In particular, I wanted to explore the ability to pass somewhat arbitrary keyword arguments (**kwargs) to the decorator function itself, that could then be used to modify the behavior of the wrapper (and, possibly, the wrapped function too): this is in preparation for a project of mine that needs to enable fine-grained permission management.
The key takeaways for today are that:
- this is possible, if a bit convoluted (but, once you get the gist of it, internally consistent);
- doing so requires some care around the syntax (in particular, the use of trailing parentheses for the decorator);
- it is possible to do so in a manner that is largely transparent to both the wrapped function and its invocation.
The full code snippet is in this gist, a few comments follow:
A wrapper inside a wrapper inside a decorator?
Note that there are two methods nested inside the “loggable“ decorator:
def loggable(**dec_kwargs): # ... def outer_wrapper(func): @wraps(func) def log_func(*args, **kwargs): # ...
the ‘wrapped’ function (“func“) is not passed in to the outer decorator, but only to the first inner method.
whose kwargs are these?
Both “loggable“ and “outer_wrapper“ are invoked at the point of declaration not invocation: the actual wrapper method (“log_func“ here) is, instead invoked in place of the decorated function (“func“) – in most cases it will eventually go round to invoke it too (with the passed in “args“ and “kwargs“; possibly modified) although it is not required to do so (see, for example, the mocks framework).
**dec_kwargs are the decorator’s keyword arguments, not the function’s; equally, **kwargs are the decorated function’s arguments, not the decorator’s:
@loggable(snoop=True, have_warrant='whocares', is_legal='barely') def batter(name, **kwargs): # ... batter('ugo', decoy=1, noise=2, val=3)
when in the body of the decorator, dec_kwargs are {snoop=True, have_warrant=’whocares’, is_legal=’barely’} while inside log_func() kwargs is {decoy=1, noise=2, val=3} (and args = [‘ugo’]).
you don’t know what you don’t know (and usually don’t care either)
By using **kwargs (and *args) when coding decorators/wrappers, one can focus only of those arguments (keyword or otherwise) that matter, and transparently pass on the others to the decorated function – interestingly enough, the wrapper has access to the return value too, which can be inspected, modified and even, in extreme cases, withheld entirely from the caller.
On the other hand, as a caller (client) of the decorated method, one can happily ignore all of the gory details and, to a large extent, even that the function is a decorated one (let alone what the decorator does) – in fact, it is best practice to design decorators that are transparent to their decorated functions and their users.
Unfortunately, this all comes crashing down when subtle bugs are introduced, and one has to step-debug through a decorator’s code to figure out what the heck it’s going on that causes an otherwise perfectly valid method to fail (as it happened to us recently).
be wary of those pesky parentheses
There is a not-so-subtle (but hardly obvious) difference between:
@loggable()
and:
@loggable
The former will work with the code presented above and in the gist, the latter will not: the difference being that, in the first case the Python compiler will not pass the decorated callable to the decorator, but to the callable that the decorator returns: that explains the rigmarole detailed above in the “wrapper inside a wrapper” section.
The latter will – a callable (the decorated method) will be passed to the decorator as the first (and only) positional argument, thus causing something like:
TypeError: loggable() takes exactly 0 arguments (1 given)
Decorators are fun!
Leave a Reply