Nested decorator functions in Python
When I was at PyConIE last October I was talking with an old friend about Python’s decorator functions.
He lamented how you need to google around for tutorials any time you wanted to write a parametrised decorator because it can be so confusing. I told him that there was a way to do it by nesting decorator functions which is much simpler than implementing them using classes (which seems to be the widely known about way).
I thought I’d write up this quick blog post with some examples that will demonstrate how to do this and serve as a reference in case I forget any of this stuff myself!
Using classes
So here’s a rick rolling example using classes:
class WithoutParams(object):
def __init__(self, func):
"""
Constructor receives target function.
"""
self.func = func
def __call__(self, *args, **kwargs):
"""
Arguments intended for target function are passed to __call__.
From here you can call the target any way you see fit.
"""
self.func("Never gonna give you up")
class WithParams(object):
def __init__(self, val):
"""
Constructor takes decorator params instead of target.
"""
self.val = val
def __call__(self, func):
"""
Target function is passed in here instead.
This is where we create a wrapper function to replace the target.
"""
def wrapped(*args, **kwargs):
"""
Wrapper function takes the target arguments and calls target.
"""
func(self.val)
return wrapped
@WithoutParams
def a(text):
print text
@WithParams("Never gonna let you down")
def b(text):
print text
if __name__ == "__main__":
a("hello world")
b("foo bar")
Using nested functions
And here’s the corresponding example which uses nested functions:
def without_params(func):
"""
Outer function takes target function and returns a wrapped one.
"""
def _without_params(*args, **kwargs):
"""
Inner function takes target arguments and makes the call.
"""
return func("Never gonna run around")
return _without_params
def with_params(val):
"""
If you need to take params, it's the same but wrapped in another function.
This one takes the decorator parameters and returns a doubly wrapped function.
"""
def _with_params(func):
def __with_params(*args, **kwargs):
return func(val)
return __with_params
return _with_params
@without_params
def a(text):
print text
@with_params("and desert you!")
def b(text):
print text
if __name__ == "__main__":
a(2, 3)
b("fizz bang")
Conclusion
Class decorators are confusing because arguments and functions are sent to different places depending on the context. Nested function decorators are a neater abstraction because the base decorator is the same in both cases, if you want parameters you just wrap it in an additional function.
Hope this comes in handy!