As I\'ve understood it there are two ways to do a Python decorator, to either use the __call__ of a class or to define and call a function as the decorator. Wha
I will dare to offer a different approach to the problem almost seven years after the question was originally made. This version is not described in any of the previous (very nice!) answers.
The biggest differences between using classes and functions as decorators are already very well described here. For the sake of completeness I'll go briefly through this again, but to be more practical, I'm going to use a concrete example.
Let's say you want to write a decorator to cache the result of "pure" functions (those free of side effects, so the return value is deterministic, given the arguments) in some cache service.
Here are two equivalent and very simple decorators for doing this, in both flavors (functional and object oriented):
import json
import your_cache_service as cache
def cache_func(f):
def wrapper(*args, **kwargs):
key = json.dumps([f.__name__, args, kwargs])
cached_value = cache.get(key)
if cached_value is not None:
print('cache HIT')
return cached_value
print('cache MISS')
value = f(*args, **kwargs)
cache.set(key, value)
return value
return wrapper
class CacheClass(object):
def __init__(self, f):
self.orig_func = f
def __call__(self, *args, **kwargs):
key = json.dumps([self.orig_func.__name__, args, kwargs])
cached_value = cache.get(key)
if cached_value is not None:
print('cache HIT')
return cached_value
print('cache MISS')
value = self.orig_func(*args, **kwargs)
cache.set(key, value)
return value
I guess this is fairly easy to understand. It's just a silly example! I'm skipping all error handling and edge cases for simplicity. You should not ctrl+c/ctrl+v code from StackOverflow anyways, right? ;)
As one can notice, both versions are essentially the same. The object oriented version is a bit longer and more verbose than the functional one, because we have to define methods and use the variable self, but I would argue it is slightly more readable. This factor becomes really important for more complex decorators. We'll see that in a moment.
The decorators above are used like this:
@cache_func
def test_one(a, b=0, c=1):
return (a + b)*c
# Behind the scenes:
# test_one = cache_func(test_one)
print(test_one(3, 4, 6))
print(test_one(3, 4, 6))
# Prints:
# cache MISS
# 42
# cache HIT
# 42
@CacheClass
def test_two(x, y=0, z=1):
return (x + y)*z
# Behind the scenes:
# test_two = CacheClass(test_two)
print(test_two(1, 1, 569))
print(test_two(1, 1, 569))
# Prints:
# cache MISS
# 1138
# cache HIT
# 1138
But let's say now that your cache service supports setting the TTL for each cache entry. You would need to defined that on decoration time. How to do it?
The traditional functional approach would be to add a new wrapper layer that returns a configured decorator (there are nicer suggestions in the other answers to this question):
import json
import your_cache_service as cache
def cache_func_with_options(ttl=None):
def configured_decorator(*args, **kwargs):
def wrapper(*args, **kwargs):
key = json.dumps([f.__name__, args, kwargs])
cached_value = cache.get(key)
if cached_value is not None:
print('cache HIT')
return cached_value
print('cache MISS')
value = f(*args, **kwargs)
cache.set(key, value, ttl=ttl)
return value
return wrapper
return configured_decorator
It is used like this:
from time import sleep
@cache_func_with_options(ttl=100)
def test_three(a, b=0, c=1):
return hex((a + b)*c)
# Behind the scenes:
# test_three = cache_func_with_options(ttl=100)(test_three)
print(test_three(8731))
print(test_three(8731))
sleep(0.2)
print(test_three(8731))
# Prints:
# cache MISS
# 0x221b
# cache HIT
# 0x221b
# cache MISS
# 0x221b
This one is still okay, but I have to admit that, even being an experienced developer, sometimes I see myself taking a good amount of time to understand more complex decorators that follow this pattern. The tricky part here is that is really not possible to "un-nest" the functions, as the inner functions need the variables defined in the scope of the outer ones.
Can the object oriented version help? I think so, but if you follow the previous structure for the class-based one, it would end up with the same nested structure as the functional one or, even worse, using flags to hold the state of what the decorator is doing (not nice).
So, instead of receiving the function to be decorated in the __init__ method and handling the wrapping and decorator parameters in the __call__ method (or using multiple classes/functions to do so, which is too complex to my taste), my suggestion is to handle the decorator parameters in the __init__ method, receive the function in the __call__ method and finally handle the wrapping in an additional method that is returned by the end of the __call__.
It looks like this:
import json
import your_cache_service as cache
class CacheClassWithOptions(object):
def __init__(self, ttl=None):
self.ttl = ttl
def __call__(self, f):
self.orig_func = f
return self.wrapper
def wrapper(self, *args, **kwargs):
key = json.dumps([self.orig_func.__name__, args, kwargs])
cached_value = cache.get(key)
if cached_value is not None:
print('cache HIT')
return cached_value
print('cache MISS')
value = self.orig_func(*args, **kwargs)
cache.set(key, value, ttl=self.ttl)
return value
The usage is as expected:
from time import sleep
@CacheClassWithOptions(ttl=100)
def test_four(x, y=0, z=1):
return (x + y)*z
# Behind the scenes:
# test_four = CacheClassWithOptions(ttl=100)(test_four)
print(test_four(21, 42, 27))
print(test_four(21, 42, 27))
sleep(0.2)
print(test_four(21, 42, 27))
# Prints:
# cache MISS
# 1701
# cache HIT
# 1701
# cache MISS
# 1701
As anything is perfect, there are two small drawbacks with this last approach:
It's not possible to decorate using @CacheClassWithOptions directly. We have to use parenthesis @CacheClassWithOptions(), even if we don't want to pass any parameter. This is because we need to create the instance first, before trying to decorate, so the __call__ method will receive the function to be decorated, not in the __init__. It is possible to work around this limitation, but it's very hacky. Better to simply accept that those parenthesis are needed.
There's no obvious place to apply the functools.wraps decorator on the returned wrapped function, what would be a no-brainer in the functional version. It can easily be done, though, by creating an intermediary function inside __call__ before returning. It just doesn't look that nice and it's better to leave that out if you don't need the nice things that functools.wraps does.