I have been learning about decorators recently. It is interesting functionality, but also one of those areas where the issues just keep appearing, the more you look at it.
To my mind, a key requirement of decorators, after the need to be useful, is to be as unintrusive as possible. So the decorated function or method, and more particularly, users of the decorated function or method should not notice the decoration.
Some of the issues, in order of increasing complexity:
- functions and methods
- return values
- function / method arguments and keyword arguments
- attributes such as __name__ and __doc__
- decorator arguments
- multiple decorators
I don’t have all the answers yet, and I’ll write more in a later post.
An example implementation shows some of the points of interest. This is a decorator with arguments implemented using a function.
from functools import wraps def decoratorWithArguments(decoratorArgument = None): def wrap(functionOrMethod): # Support naive introspection @wraps(functionOrMethod) def wrappedFunctionOrMethod(*args, **kwargs): print("Before %s" % str(decoratorArgument) ) ret = functionOrMethod(*args, **kwargs) print("After %s" % str(decoratorArgument) ) return ret return wrappedFunctionOrMethod return wrap @decoratorWithArguments("Decorator Argument Value") def functionExample(): """ functionExample docstring """ return "functionExample" class ClassExample: @decoratorWithArguments() # defaults to None def methodExample(self): """ methodExample docstring """ return "methodExample" print(functionExample()) classInstance = ClassExample() print(classInstance.methodExample())
- the wraps function from functools copies across various attributes such as the docstring.
- *args and **kwargs should cope with both positional arguments and keyword arguments
- Because decorators work differently with and without arguments, even though this decorator will work without any arguments, I still need to use () on line 22.
It took me an age to understand how decoratorWithArguments could possibly work. It’s a bit like Russian dolls:
- The outer function, decoratorWithArguments, takes the decorator arguments. It returns the inner function, wrap (a closure).
- Having unwrapped the outer function, we now have the returned inner function (wrap). This can then be called with the function or method that we want to decorate. It returns the inner function, wrappedFunctionOrMethod.
- wrappedFunctionOrMethod can then be called. It will be called in place of the original decorated function or method.
Note that the decoration process takes care of all of the above for us. It’s still nice to understand how it can work.