Decorators a very useful tool in Python, but they are hard to implement in ways that don’t have some nasty gotchas. Overall I think that this is a spot in Python that could use a little rewrite but that is a topic for another day. This article is documentation of one way through this that tries to avoid almost all the potholes.
Edit 4/4/2019: I just stumbled across another article that references the “Python cookbook 3rd edition” as having another solution similar to mine (search for “Allow for optional arguments” on that page).
Decorators can be invoked three different ways (please note the style identifiers):
@my_decorator # Style 1: without parentheses
def a():
pass
@my_decorator() # Style 2: with parentheses, but without any values
def b():
pass
@my_decorator(bob=1) # Style 3: with parentheses and some values
def c():
pass
And they can be written in two forms: Class style
and Function style. When it comes to applying them
to functions, both work on all three styles above. However, for methods (functions attached to
Object instances), the first style does not work at all. For a discussion on that see my page on
the problem.
But it is possible to make a decorator that works on all three of these styles, and works on
methods as well as functions, but it takes a some thinking. Rather than go through all that
work, you can just start with my version:
import functools
def my_decorator(*args, **kwargs):
func = None
if len(args) == 1 and callable(args[0]) and not kwargs:
func = args[0]
args = tuple()
def outer(func):
# note: `args` and `kwargs` are available here
@functools.wraps(func)
def inner(*inner_args, **inner_kwargs):
# note: `args` and `kwargs` are available here too
func(*inner_args, **inner_kwargs)
return inner
if func:
return outer(func)
else:
return outer
The only limitation I am aware of is that you can’t call call the decorator with a single
callable. So you can’t do this:
@my_decorator(lambda x: x)
def my_function(a):
pass
I could not figure out a way of seperating that from the no-parentheses case. However, you could use a throw-away second value, or keyword to get around this, so:
@my_decorator(lambda x: x, waste=None)
def my_function(a):
pass
Annoying, but probably not a limitation for most uses.