Understanding Python decorators

Problem: Some testers shy away from unit testing because they are uncomfortable with the concept of annotations.


Why this post?

We often stop learning a topic when we need to apply a concept that we are not fully comfortable with. I noticed this pattern in testers when it came to unit testing. A lot of unit testing frameworks, like JUnit and PyUnit, make use of annotations. Testers who are uncomfortable with the concept of annotations are less inclined to learn unit testing. With this blog, we aim to clarify annotations for testers. We hope this post helps you in learning and exploring unit testing frameworks.

NOTE: In the Python world, annotations are called decorators.


What is a decorator?

Decorators are a metaprogramming feature that lets you modify the language itself. A decorator is basically a wrap function. It takes a function as an argument and returns a replacement function. In general, a decorator is used to modify the behavior of function or classes and perform some additional actions on it. Do not worry if you did not relate to this semi-technical definition. In this post I provide an example of where we humans naturally use the concept of decorators outside of code. I’ll also give some scenarios where you can use decorators.


Decorators in the real world:

When I think of decorators in the real world, I think of a gun. What?? A Gun?! Before you think of shooting me let me clarify. There are a lot of accessories you can use with a gun like silencers, flashlights, laser scopes, stocks, pistol grips, etc.
Gun with silencer
Now think of a gun which is wrapped with a silencer. The silencer modifies the behavior of the gun and performs some additional action like suppressing the noise. The silencer extends the functionality of gun without modifying it. It makes it easy to turn on and turn off the additional features.
Python decorator shooting cartoon


Why use decorators at all?

Key reasons to use decorators are:
1. Improve code readability
2. Extend functionality of a function

1. Improve code readability:
Using Python decorators, we can make the code look cleaner. Consider the below example

def addSixtyFour(f):
    def inner():
        return f() +64
    return inner
 
def foo():
    return 5
 
foo = addSixtyFour(foo)
print foo()

With the addition of the @ decoration operator, you now get the same result instead of using foo = addSixtyFour(foo)

def addSixtyFour(f):
    def inner():
        return f() +64
    return inner
 
@ addSixtyFour
def foo():
    return 5
 
print foo()

Python replaces replaces foo() function with the decorated function addSixtyFour(). The decorated function addSixtyFour() has access to foo() in the form of f. Without decorators we have to do something much less elegant to achieve the same thing.

2. Extend functionality of a function:
Decorators are ideally suited when you want to extend the functionality of function without modifying the function. It makes sense to use decorators if we don’t want to change the original function. Reverting back to the old functionality is easy. To revert back to the old functionality, just remove the decorator.


Frequently used decorators

In this section I cover a few frequently used decorators. For the examples below, instead of decorating a function with another function we will decorate it with a class. The object returned by the decorator can be used as a function which basically means it must be callable. So any classes we use as decorators must implement __call__ method. We also need to have __init__ method so that it can take the target function as a first-class object into the class.

NOTE: If you have forgotten to implement the __call__ method, you will see the error message: TypeError: $decorator_name object is not callable

Example 1: Try catch
In the below example, I have used a decorator called “errorHandler” to handle the exception related to division by zero

class errorHandler(object):
  def __init__(self, f):
    self.f = f
 
  def __call__(self, *args):
    try:
      return self.f(*args)
    except Exception, e:
      print "Error: %s" % (e)
 
@errorHandler
def divide(x):
  return 1 / x
 
divide(0)

Example 2: Entry exit
If you want any function to perform some actions at entry and exit of the function you can use a decorator similar to this.

class entryExit(object):
 
    def __init__(self, f):
        self.f = f
 
    def __call__(self):
        print "Entering", self.f.__name__
        self.f()
        print "Exiting", self.f.__name__
 
@entryExit
def func1():
    print "inside func1()"
 
@entryExit
def func2():
    print "inside func2()"
 
func1()
func2()

The decorated functions now have the “Entering” and “Exiting” statements around the call. The constructor stores the argument, which is the function object. In the call, we use the __name__ attribute of the function to display that function’s name, then call the function itself.

Example 3: Unittest skip decorator
Unittest supports skipping individual test methods and even whole classes of tests. Skipping a test is simply a matter of using the skip() decorator or one of its conditional variants like skipIf() and skipUnless().

import unittest
class MyTestCase(unittest.TestCase):
 
    @unittest.skip("demonstrating skipping")
    def testSkip(self):
        self.fail("shouldn't happen")
 
    @unittest.skipIf(True,"skip function since it is true")
    def testSkipIf(self):
        # Test that work if condition is false.
        pass
 
    @unittest.skipUnless(False, "skip function since it is False")
    def testSkipUnless(self):
        # Test that work unless condition is True.
        pass
 
if __name__ == '__main__':
    unittest.main()

We hope this post de-mystified Python decorators for you. As always, we are happy to clarify any questions you may have. Fire away!


One thought on “%1$s”

  1. HI Avinash Shetty,
    This is an excellent piece of work. I have been trying to understand the decorators for while but i could not get into my brain. But you did in very simple and easy to understand.
    Please keep it up your work.
    thanks for your help

Leave a Reply

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