Topics
Decorators in python are a convenient way to allow us to wrap a func or class with additional functionality. Best explained with an example. Say I want to cache few functions in my codebase. Without decorators, I would probably create some function like this:
cache = {}
def cached(func, *args):
try:
return cache[args]
except KeyError:
cache[args] = ret = func(*args)
return cache[args]
def norm(vec):
print("Computing...")
return math.sqrt(sum(i*i for i in vec))
vec = (3, 4)
vec_norm = cached(norm, vec)
vec_norm = cached(norm, vec) # doesn't print "Computing", uses cache
print(vec_norm)
print(cache)
This is fine, but we had to change the API for the user: norm(...)
vs cached(norm, ...)
and user has to remember this change. This is where decorators help us:
import math
cache = {}
def cached(func, *args):
try:
return cache[args]
except KeyError:
cache[args] = ret = func(*args)
return cache[args]
# our "decorator", using closures
def wrapper(func):
def inner(*args):
return cached(func, *args)
return inner
def norm(vec):
print("Computing...")
return math.sqrt(sum(i*i for i in vec))
# our decorated func
wrapped_norm = wrapper(norm)
vec = (3, 4)
vec_norm = wrapped_norm(vec)
vec_norm = wrapped_norm(vec)
print(vec_norm)
print(cache)
We basically wrap/decorate our original function in above sample code, leveraging closures. It works exactly the same as before, but our function signature is much better now: norm
vs wrapped_norm
. User now only has to remember this new function name. Python provides even more convenience by allowing us to annotate the original function with our wrapper
:
# Instead of:
wrapped_norm = wrapper(norm)
vec_norm = wrapped_norm(vec)
# We do:
@wrapper
def norm(vec):
print("Computing...")
return math.sqrt(sum(i*i for i in vec))
vec_norm = norm(vec)
This allows us to re-use the same function name but now with modified behavior. There are several use-cases: logging entry-exit, wrapping try-except, caching, func call counting etc.
We can also create decorators by defining a class instead of a function:
class wrapper:
def __init__(self, func):
self.func = func
def __call__(self, *args):
return cached(self.func, *args)
So far, we have seen ways to decorate functions, but it’s also possible (and applicable) to decorate classes and even pass args/flags to these decorators, i.e. @cached(max_size=3)
etc.