Python decorators for dummies

If you’re going through interviews for the positions of Python developer, or looking forward to preparing for one, or just a curios developer, you better have your head clear around the concept of decorators in Python programming language.
I won’t be delving into ‘what are design patterns’ , and why should you make use of it, whenever possible. The post is merely about understanding and writing decorators in Python. You can find plethora of posts about Python decorators, the motivation for me is, that everyone has their own way of explaining, especially a technical concept.

All you experienced developers you need to keep calm, as I am starting with very basics, to make this post work for even the newbies.

What is a decorator

In simplest words, decorator is a function returning another function, and it is applied to a function using the @decorator_name syntax just above the function definition line, for example: function hello() has been decorated with @hello_decorator.
@hello_decorator
def hello:
    print("Hello from original")

How to write a decorator

Decorator are written the same way as you write other function definitions.
def hello_decorator(original_fn):
    def decorator_fn():
        print("Hello from new")
    return decorator_fn
  • Line 1 – the decorator function definition, the point to note is, a decorator will always has the original function (the function it is decorating) as it’s argument i.e. original_fn
  • Line 2 – The new function, via which the new functionality will be added.
  • Line 3 – Some functionality we need to add (decorate original function with), here we’re printing “Hello from new”
  • Line 4 – Return the new function – decorated_fn

Output

>>> hello()
Hello from new
You may have noticed, that the print in the original function (hello) did not appear/executed. Yes you’re right, as we are required to execute the original function explicitly inside the new function (decorated). The above code will become:

 

def hello_decorator(original_fn)
    def decorator_fn():
        print("Hello from new")
        original_fn()   # original function must be invoked
    return decorator_fn

@hello_decorator
def hello():
   print("Hello from original")

Output

>>> hello()
Hello from new
Hello from original

Function with arguments

Now you know how to write a decorator, let’s try another example.
def add_decorator(original_fn):
    def decorator_fn():
        print("Hello from new")
        original_fn()
    return decorator_fn
 
@add_decorator
def add(x, y):
    print(x + y)

Output

>>> add(2, 2)
TypeError: decorated() takes 0 positional arguments but 2 were given
The above error is correct, we need to cater the 2 arguments that are passed to add(). The inner function has to handle any arbitrary number of arguments using Python’s standard *args and **kwargs, for arbitrary positional and keyword arguments, respectively.
def add_decorator(original_fn):
    def decorator_fn(*args, **kwargs):
        print("Hello from new")
        original_fn(*args, **kwargs)
    return decorator_fn
 
@add_decorator
def add(x, y):
    print(x + y)

Output

>>>  add(2, 2)
Hello from new
4

Decorator with arguments

Above we have catered the arguments for original function, and now let’s look into the concept that decorators can take arguments too. This is important as it adds more power into decorators i.e. we can write generic decorators to be used by different functions in their specific contexts.
def add_decorator(n):
    def decorator_fn(original_fn):
        def wrapper_fn(*args, **kwargs):
            result = original_fn(*args, **kwargs)
            print(result+n)
            return result + n
        return wrapper_fn
    return decorator_fn
 
@add_decorator(2)
def add(x, y):
    return x + y

Output

>>> add(2, 2)
6
The execution prints 6, adding the 2, given as an argument to the decorator. The above examples are for understanding purpose only, no way this will be a use-case for writing a decorator.

When to use a decorator

I can’t state a set of rules for when to use a decorator, instead I am sharing few examples (lot of possibilities) you can make an efficient use of a decorator, in your day-to-day software development.

1. Exception handling example

def exception_handler(original_fn):
    def decorator_fn(*args, **kwargs):
        try:
            return original_fn(*args, **kwargs)
        except Exception as err:
            print(err)
    return decorator_fn
 
@exception_handler
def add(x, y):
    sum = x + y
    print(sum)
    return sum

Output

>>> add(2, 2)
4
>>>
>>> add(2, "a")
unsupported operand type(s) for +: 'int' and 'str'
It has printed the exception message in second case, this way you can a very generic exception handler to be used for all your functions. You can add more power by passing a logger to the decorator i.e. @exception_handler(logger), and use it inside the execpt block to log exception or even to whole traceback (recommended).

2. Timer example

import time
 
def timeit(original_fn):
    def decorator_fn(*args, **kwargs):
        start = time.time()
        original_fn(*args, **kwargs)
        end = time.time()
        print('func:%r args:[%r, %r] took: %2.6f sec' % (original_fn.__name__,
                                                         args, kwargs, end - start))
 
    return decorator_fn
 
@timeit
def add(x, y):
    return x + y

Output

>>> add(2, 2)
func:'add' args:[(2, 2), {}] took: 0.000001 sec
>>>
>>> multiply(2, 2)
func:'multiply' args:[(2, 2), {}] took: 0.000002 sec

Multiple decorators

You can decorate a function with multiple decorators i.e. can stack more than one decorator over a function definition. The top one will be executed last in the stack. Here’s a link if you’re curious about visualizing the execution stack, in which order the function and decorators will be executed.
def add_decorator_one(original_fn):
    def decorator_fn(*args, **kwargs):
        original_fn(*args, **kwargs)
        print("Hello from one")
    return decorator_fn
 
def add_decorator_two(original_fn):
    def decorator_fn(*args, **kwargs):
        original_fn(*args, **kwargs)
        print("Hello from two")
    return decorator_fn
 
 
@add_decorator_one
@add_decorator_two
def add(x, y):
    print(x + y)

Output

>>> add(2, 2)
4
Hello from two
Hello from one

Few ready-made decorators

@staticmethod

Is used to specify a class method as a static i.e. it can be called without instantiation.
class A(object):
    def foo(self, x):
        print("foo - %s" % x)
 
    @staticmethod
    def static_foo(x):
        print("static foo - %s" % x)

Output

>>> A.foo(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() missing 1 required positional argument: 'x'
>>> A.static_foo(1)
static foo - 1
Other commonly used built-in Python decorators are @classmethod, @property for specifying the property with get-er and set-er

Leave a Reply

Your email address will not be published. Required fields are marked *