Python decorator best practice, using a class vs a function
It is rather subjective to say whether there are "advantages" to each method.
However, a good understanding of what goes under the hood would make it natural for one to pick the best choice for each occasion.
A decorator (talking about function decorators), is simply a callable object that takes a function as its input parameter. Python has its rather interesting design that allows one to create other kinds of callable objects, besides functions - and one can put that to use to create more maintainable or shorter code on occasion.
Decorators were added back in Python 2.3 as a "syntactic shortcut" for
def a(x):
...
a = my_decorator(a)
Besides that, we usually call decorators some "callables" that would rather be "decorator factories" - when we use this kind:
@my_decorator(param1, param2)
def my_func(...):
...
the call is made to "my_decorator" with param1 and param2 - it then returns an object that will be called again, this time having "my_func" as a parameter. So, in this case, technically the "decorator" is whatever is returned by the "my_decorator", making it a "decorator factory".
Now, either decorators or "decorator factories" as described usually have to keep some internal state. In the first case, the only thing it does keep is a reference to the original function (the variable called f
in your examples). A "decorator factory" may want to register extra state variables ("param1" and "param2" in the example above).
This extra state, in the case of decorators written as functions is kept in variables within the enclosing functions, and accessed as "nonlocal" variables by the actual wrapper function. If one writes a proper class, they can be kept as instance variables in the decorator function (which will be seen as a "callable object", not a "function") - and access to them is more explicit and more readable.
So, for most cases it is a matter of readability whether you will prefer one approach or the other: for short, simple decorators, the functional approach is often more readable than one written as a class - while sometimes a more elaborate one - especially one "decorator factory" will take full advantage of the "flat is better than nested" advice fore Python coding.
Consider:
def my_dec_factory(param1, param2):
...
...
def real_decorator(func):
...
def wraper_func(*args, **kwargs):
...
#use param1
result = func(*args, **kwargs)
#use param2
return result
return wraper_func
return real_decorator
against this "hybrid" solution:
class MyDecorator(object):
"""Decorator example mixing class and function definitions."""
def __init__(self, func, param1, param2):
self.func = func
self.param1, self.param2 = param1, param2
def __call__(self, *args, **kwargs):
...
#use self.param1
result = self.func(*args, **kwargs)
#use self.param2
return result
def my_dec_factory(param1, param2):
def decorator(func):
return MyDecorator(func, param1, param2)
return decorator
update: Missing "pure class" forms of decorators
Now, note the "hybrid" method takes the "best of both Worlds" trying to keep the shortest and more readable code. A full "decorator factory" defined exclusively with classes would either need two classes, or a "mode" attribute to know if it was called to register the decorated function or to actually call the final function:
class MyDecorator(object):
"""Decorator example defined entirely as class."""
def __init__(self, p1, p2):
self.p1 = p1
...
self.mode = "decorating"
def __call__(self, *args, **kw):
if self.mode == "decorating":
self.func = args[0]
self.mode = "calling"
return self
# code to run prior to function call
result = self.func(*args, **kw)
# code to run after function call
return result
@MyDecorator(p1, ...)
def myfunc():
...
And finally a pure, "white colar" decorator defined with two classes - maybe keeping things more separated, but increasing the redundancy to a point one can't say it is more maintainable:
class Stage2Decorator(object):
def __init__(self, func, p1, p2, ...):
self.func = func
self.p1 = p1
...
def __call__(self, *args, **kw):
# code to run prior to function call
...
result = self.func(*args, **kw)
# code to run after function call
...
return result
class Stage1Decorator(object):
"""Decorator example defined as two classes.
No "hacks" on the object model, most bureacratic.
"""
def __init__(self, p1, p2):
self.p1 = p1
...
self.mode = "decorating"
def __call__(self, func):
return Stage2Decorator(func, self.p1, self.p2, ...)
@Stage1Decorator(p1, p2, ...)
def myfunc():
...
2018 update
I wrote the text above a couple years ago. I came up recently with a pattern I prefer due to creating code that is "flatter".
The basic idea is to use a function, but return a partial
object of itself if it is called with parameters before being used as a decorator:
from functools import wraps, partial
def decorator(func=None, parameter1=None, parameter2=None, ...):
if not func:
# The only drawback is that for functions there is no thing
# like "self" - we have to rely on the decorator
# function name on the module namespace
return partial(decorator, parameter1=parameter1, parameter2=parameter2)
@wraps(func)
def wrapper(*args, **kwargs):
# Decorator code- parameter1, etc... can be used
# freely here
return func(*args, **kwargs)
return wrapper
And that is it - decorators written using this pattern can decorate a function right away without being "called" first:
@decorator
def my_func():
pass
Or customized with parameters:
@decorator(parameter1="example.com", ...):
def my_func():
pass
2019 - With Python 3.8 and positional only parameters this last pattern will become even better, as the func
argument can be declared as positional only, and require the parameters to be named;
def decorator(func=None, *, parameter1=None, parameter2=None, ...):
I mostly agree with jsbueno: there's no one right way. It depends on the situation. But I think def is probably better in most cases, because if you go with class, most of the "real" work is going to be done in __call__
anyway. Also, callables that are not functions are pretty rare (with the notable exception of instantiating a class), and people generally do not expect that. Also, local variables are usually easier for people to keep track of vs. instance variables, simply because they have more limited scope, although in this case, the instance variables are probably only used in __call__
(with __init__
simply copying them from arguments).
I have to disagree with his hybrid approach though. It's an interesting design, but I think it's probably going to confuse the crap out of you or someone else who looks at it a few months later.
Tangent: Regardless of whether you go with class or function, you should use functools.wraps
, which itself is meant to be used as a decorator (we must go deeper!) like so:
import functools
def require_authorization(f):
@functools.wraps(f)
def decorated(user, *args, **kwargs):
if not is_authorized(user):
raise UserIsNotAuthorized
return f(user, *args, **kwargs)
return decorated
@require_authorization
def check_email(user, etc):
# etc.
This makes decorated
look like check_email
e.g. by changing it's func_name
attribute.
Anyway, this is usually what I do and what I see other people around me doing, unless I want a decorator factory. In that case, I just add another level of def:
def require_authorization(action):
def decorate(f):
@functools.wraps(f):
def decorated(user, *args, **kwargs):
if not is_allowed_to(user, action):
raise UserIsNotAuthorized(action, user)
return f(user, *args, **kwargs)
return decorated
return decorate
By the way, I would also be on guard against excessive use of decorators, because they can make it really hard to follow stack traces.
One approach for managing hideous stack traces is to have a policy of not substantially changing the behavior of the decoratee. E.g.
def log_call(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
logging.debug('call being made: %s(*%r, **%r)',
f.func_name, args, kwargs)
return f(*args, **kwargs)
return decorated
A more extreme approach for keeping your stack traces sane is for the decorator to return the decoratee unmodified, like so:
import threading
DEPRECATED_LOCK = threading.Lock()
DEPRECATED = set()
def deprecated(f):
with DEPRECATED_LOCK:
DEPRECATED.add(f)
return f
@deprecated
def old_hack():
# etc.
This is useful if the function is called within a framework that knows about the deprecated
decorator. E.g.
class MyLamerFramework(object):
def register_handler(self, maybe_deprecated):
if not self.allow_deprecated and is_deprecated(f):
raise ValueError(
'Attempted to register deprecated function %s as a handler.'
% f.func_name)
self._handlers.add(maybe_deprecated)