Python decorator pattern: reducing code duplication involving inner functions and functools.wraps

519 views Asked by At

I'm seeing a lot of documentation on StackOverflow and elsewhere about how to write Python decorators. They typically recommend using functools.wraps and (potentially multiple) inner functions. Especially complex is if I want a decorator which can either be called with or without brackets, i.e. @foo or @foo(bar).

For example, this StackOverflow question and its various answers give a lot of insight into how to do this. However, they all appear rather surprisingly complicated (either extra conditional logic or deeper nesting of functions) for something which seems conceptually simple. My biggest concern is that, in all the examples given, >50% of the code is unrelated to that particular decorator's behavior and is shared boilerplate among all decorators written using that pattern.

The real-world examples I was looking at were the Fabric project's decorators.py and some of the Django project's various decorator.py instances. Seems a bit strange to me that there is a lot of boilerplate code unrelated to the actual intent.

I understand why I would want to use functools.wraps for code maintainability reasons, but this seems overly complex. Is there some way to DRY and/or encapsulate this code? It seems like the only part that I really care about from the perspective of user code is the actual body of the generated inner function. How could I go about writing the remaining boilerplate once and reusing it forever?

Thanks in advance!

2

There are 2 answers

1
Steven Kryskalla On BEST ANSWER

The decorator, wrapt, and decorators packages on PyPI abstract out some of that boilerplate code beyond what functools.wraps provides.

If you want to use both @foo and @foo(args), you will necessarily incur extra complexity in your code, since foo(func) and foo(args)(func) are completely different ways to wrap a function. I agree with Pete that @foo() is clearer in that case.

1
Pete Cacioppi On

I think the functools.wraps does represent the boilerplate abstraction. I don't think it's that bothersome. I generally code up a decorator without it, test it, then check that the tests still work after weaving in functools.wraps. Adds like 15 minutes tops.

That said, I don't think it's necessary to use functools wraps for every decorator. For functions that are purely internal, and so self explanatory that they don't need a docstring, I often don't bother insisting that their decorations perform a full wrap.

Finally, I believe very strongly that in situations where @foo(bar) is an option, you create foo to be a "decorator factory". (I.e. a function that returns a decorator). Then, you'd use @foo() or @foo(bar). I think this is the correct way to handle decorators that are parameterized beyond their single argument. To my mind, a decorator is always function that takes one argument and returns a modification and/or wrap of that argument.