Apcelent
Posted on July 9, 2018
If you want to take a really deep dive, you should read these exhaustive articles by Graham Dumpleton. However, if you intend to get started and getting better at reading/writing python decorators, this article should suffice.
Everything is an object in python, even functions. A function can be assigned to a variable, passed to another function and can be returned from another function. Take a look at the example below:
[1]: def outer_function():
...: print "1. This is outer function!"
...: def inner_function():
...: print "2. This is inner function, inside outer function!"
...: print "3. This is outside inner function, inside outer function!"
...: return inner_function()
...:
[2]: func_assign = outer_function()
1. This is outer function!
3. This is outside inner function, inside outer function!
2. This is inner function, inside outer function!
Mark in the above execution, how the statement inside the inner function is printed at the last, consequential to inner_function being returned, at the end of outer_function, and the execution could be seen during the assignment.
Python decorator are the function that receive a function as an argument and return another function as return value. The assumption for a decorator is that we will pass a function as argument and the signature of the inner function in the decorator must match the function to decorate.
Function Decorator
Let us now, write a simple function decorator for ourselves. We will write a decorator that would measure the execution time of the function passed to it.
import time
def timetest(input_func):
def timed(*args, **kwargs):
start_time = time.time()
result = input_func(*args, **kwargs)
end_time = time.time()
print "Method Name - {0}, Args - {1}, Kwargs - {2}, Execution Time - {3}".format(
input_func.__name__,
args,
kwargs,
end_time - start_time
)
return result
return timed
@timetest
def foobar(*args, **kwargs):
time.sleep(0.3)
print "inside foobar"
print args, kwargs
foobar(["hello, world"], foo=2, bar=5)
inside foobar
(['hello, world'],) {'foo': 2, 'bar': 5}
Method Name - foobar, Args - (['hello, world'],), Kwargs - {'foo': 2, 'bar': 5}, Execution Time - 0.30296087265
We passed the function foobar to decorator named timetest. Inside decorator, function foobar is referenced as variable input_func. The result, post execution of input_func is referred as result.
Prepending @ to the name of the decorator, and writing the same above a function calls the decorator, and passes the function to the decorator(decorates).
Method Decorator
Method decorators allow overriding class properties by decorating, without having to find the calling function.
def method_decorator(method):
def inner(city_instance):
if city_instance.name == "SFO":
print "Its a cool place to live in."
else:
method(city_instance)
return inner
class City(object):
def __init__(self, name):
self.name = name
@method_decorator
def print_test(self):
print self.name
p1 = City("SFO")
p1.print_test()
Its a cool place to live in.
In the snippet shown above, we decorate the class method print_test. The method_decorator prints the name of the city, if the name of city instance is not SFO.
Class Decorators
If you want to create a callable returning another callable, the function decorator approach is easier. If you want the return to be a function, function decorators should be preferred, however if you want the decorator to return a custom object that does something different to what a function does, in that case a class decorator should be used.
With a class, you can add methods and properties to the decorated callable object, or implement operations on them. You can create descriptors that act in a special way when placed in classes (e.g. classmethod, property)
class decoclass(object):
def __init__(self, f):
self.f = f
def __call__(self, *args, **kwargs):
# before f actions
print 'decorator initialised'
self.f(*args, **kwargs)
print 'decorator terminated'
# after f actions
@decoclass
def klass():
print 'class'
klass()
Chaining Decorators
The chaining of decorator is similar to how multiple inheritance can be used to construct classes We can write as many decorator as we want and include them one by one in decoration line with decoration syntax before the definition of function to be decorated.
def makebold(f):
return lambda: "<b>" + f() + "</b>"
def makeitalic(f):
return lambda: "<i>" + f() + "</i>"
@makebold
@makeitalic
def say():
return "Hello"
print say()
One thing should be kept in mind that the order of decorators we set matters. When you chain decorators, the order in which they are stacked is bottom to top.
Functools and Wraps
When we use a decorator, we are replacing one functions with another.
def decorator(func):
"""decorator docstring"""
def inner_function(*args, **kwargs):
"""inner function docstring """
print func.__name__ + "was called"
return func(*args, **kwargs)
return inner_function
@decorator
def foobar(x):
"""foobar docstring"""
return x**2
If we try printing the name and docstring we see the following
print foobar.__name__
print foobar.__doc__
inner_function
inner function docstring
The above observation leads us to conclude that the function foobar is being replaced by inner_function. This means that we are losing information about the function which is being passed. functools.wraps comes to our rescue. It takes the function used in the decorator and adds the functionality of copying over the function name, docstring, arguemnets etc. Lets decorate without losing information:
from functools import wraps
def wrapped_decorator(func):
"""wrapped decorator docstring"""
@wraps(func)
def inner_function(*args, **kwargs):
"""inner function docstring """
print func.__name__ + "was called"
return func(*args, **kwargs)
return inner_function
@wrapped_decorator
def foobar(x):
"""foobar docstring"""
return x**2
print foobar.__name__
print foobar.__doc__
foobar
foobar docstring
The above implementation preserves the information about the funciton being passed to the decorator.
How would you go about caching information inside a class based decorator?
One of the ways of doing it, is listed here , would love to see more implementation, in comments.
Decorators with Arguments
Function Decorator with Arguments
from functools import wraps
def decorator(arg1, arg2):
def inner_function(function):
@wraps(function)
def wrapper(*args, **kwargs):
print "Arguements passed to decorator %s and %s" % (arg1, arg2)
function(*args, **kwargs)
return wrapper
return inner_function
@decorator("arg1", "arg2")
def print_args(*args):
for arg in args:
print arg
print print_args(1, 2, 3)
Arguements passed to decorator arg1 and arg2
1
2
3
Class Based Decorators with Arguments
class ClassDecorator(object):
def __init__(self, arg1, arg2):
print "Arguements passed to decorator %s and %s" % (arg1, arg2)
self.arg1 = arg1
self.arg2 = arg2
def __call__(self, foo, *args, **kwargs):
def inner_func(*args, **kwargs):
print "Args passed inside decorated function .%s and %s" % (self.arg1, self.arg2)
return foo(*args, **kwargs)
return inner_func
@ClassDecorator("arg1", "arg2")
def print_args(*args):
for arg in args:
print arg
print_args(1, 2, 3)
Arguements passed to decorator arg1 and arg2
Args passed inside decorated function .arg1 and arg2
1
2
3
How would you go about implementing decorator with optional arguments?
Try following this SO Post.
You might want to further explore the Wrapt Library.
The article originally appeared on Apcelent Tech Blog.
Posted on July 9, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.