Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

A Light Introduction to Decorators in Python


When working on code, whether we know it or not, we often come across the Decorator design pattern. This is a programming technique to extend the functionality of classes or functions without modifying them. The decorator design pattern allows us to mix and match extensions easily. Python has a decorator syntax rooted in the decorator design pattern. Knowing how to make and use a decorator can help you write more powerful code.

In this post, you will discover the decorator pattern and Python’s function decorators.

After completing this tutorial, you will learn:

  • What is the decorator pattern, and why is it useful
  • Python’s function decorators and how to use them

Let’s get started!

A Gentle Introduction to Decorators in Python
Photo by Olya Kobruseva. Some rights reserved.

Overview

This tutorial is divided into four parts:

  • What is the decorator pattern, and why is it useful?
  • Function decorators in Python
  • The use cases of decorators
  • Some practical examples of decorators

What is the decorator pattern, and why is it useful?

The decorator pattern is a software design pattern that allows us to dynamically add functionality to classes without creating subclasses and affecting the behavior of other objects of the same class. By using the decorator pattern, we can easily generate different permutations of functionality that we might want without creating an exponentially increasing number of subclasses, making our code increasingly complex and bloated.

Decorators are usually implemented as sub-interfaces of the main interface that we want to implement and store an object of the main interface’s type. It will then modify the methods to which it wants to add certain functionality by overriding the methods in the original interface and calling on methods from the stored object.

UML class diagram for decorator pattern

Above is the UML class diagram for the decorator design pattern. The decorator abstract class contains an object of type OriginalInterface; this is the object whose functionality the decorator will be modifying. To instantiate our concrete DecoratorClass, we would need to pass in a concrete class that implements the OriginalInterface, and then when we make method calls to DecoratorClass.method1(), our DecoratorClass should modify the output from the object’s method1().

With Python, however, we are able to simplify many of these design patterns due to dynamic typing along with functions and classes being first-class objects. While modifying a class or a function without changing the implementation remained the key idea of decorators, we will explore Python’s decorator syntax in the following.

Function Decorators in Python

A function decorator is an incredibly useful feature in Python. It is built upon the idea that functions and classes are first-class objects in Python.

Let’s consider a simple example, that is, to call a function twice. Since a Python function is an object and we can pass a function as an argument to another function, this task can be done as follows:

Again, since a Python function is an object, we can make a function to return another function, which is to execute yet another function twice. This is done as follows:

The function returned by repeat_decorator() above is created when it is invoked, as it depends on the argument provided. In the above, we passed the hello_world function as an argument to the repeat_decorator() function, and it returns the decorated_fn function, which is assigned to hello_world_twice. Afterward, we can invoke hello_world_twice() since it is now a function.

The idea of decorator pattern applies here. But we do not need to define the interface and subclasses explicitly. In fact, hello_world is a name defined as a function in the above example. There is nothing preventing us from redefining this name to something else. Hence we can also do the following:

That is, instead of assigning the newly created function to hello_world_twice, we overwrite hello_world instead. While the name hello_world is reassigned to another function, the previous function still exists but is just not exposed to us.

Indeed, the above code is functionally equivalent to the following:

In the above code, @repeat_decorator before a function definition means to pass the function into repeat_decorator() and reassign its name to the output. That is, to mean hello_world = repeat_decorator(hello_world). The @ line is the decorator syntax in Python.

Note: @ syntax is also used in Java but has a different meaning where it’s an annotation that is basically metadata and not a decorator.

We can also implement decorators that take in arguments, but this would be a bit more complicated as we need to have one more layer of nesting. If we extend our example above to define the number of times to repeat the function call:

The repeat_decorator() takes in an argument and returns a function which is the actual decorator for the hello_world function (i.e., invoking repeat_decorator(5) returns inner_decorator with the local variable num_repeats = 5 set). The above code will print the following:

Before we end this section, we should remember that decorators can also be applied to classes in addition to functions. Since class in Python is also an object, we may redefine a class in a similar fashion.

The Use Cases of Decorators

The decorator syntax in Python made the use of decorators easier. There are many reasons we may use a decorator. One of the most common use cases is to convert data implicitly. For example, we may define a function that assumes all operations are based on numpy arrays and then make a decorator to ensure that happens by modifying the input:

We can further add to our decorator by modifying the output of the function, such as rounding off floating point values:

Let’s consider the example of finding the sum of an array. A numpy array has sum() built-in, as does pandas DataFrame. But the latter is to sum over columns rather than sum over all elements. Hence a numpy array will sum to one floating point value while a DataFrame will sum to a vector of values. But with the above decorator, we can write a function that gives you consistent output in both cases:

Running the above code gives us the output:

This is a simple example. But imagine if we define a new function that computes the standard deviation of elements in an array. We can simply use the same decorator, and then the function will also accept pandas DataFrame. Hence all the code to polish input is taken out of these functions by depositing them into the decorator. This is how we can efficiently reuse the code.

Some Practical Examples of Decorators

Now that we learned the decorator syntax in Python, let’s see what we can do with it!

Memoization

There are some function calls that we do repeatedly, but where the values rarely, if ever, change. This could be calls to a server where the data is relatively static or as part of a dynamic programming algorithm or computationally intensive math function. We might want to memoize these function calls, i.e., storing the value of their output on a virtual memo pad for reuse later.

A decorator is the best way to implement a memoization function. We just need to remember the input and output of a function but keep the function’s behavior as-is. Below is an example:

In this example, we implemented memoize() to work with a global dictionary MEMO such that the name of a function together with the arguments becomes the key and the function’s return becomes the value. When the function is called, the decorator will check if the corresponding key exists in MEMO, and the stored value will be returned. Otherwise, the actual function is invoked, and its return value is added to the dictionary.

We use pickle to serialize the input and output and use hashlib to create a hash of the input because not everything can be a key to the Python dictionary (e.g., list is an unhashable type; thus, it cannot be a key). Serializing any arbitrary structure into a string can overcome this and guarantee that the return data is immutable. Furthermore, hashing the function argument would avoid storing an exceptionally long key in the dictionary (for example, when we pass in a huge numpy array to the function).

The above example uses fibonacci() to demonstrate the power of memoization. Calling fibonacci(n) will produce the n-th Fibonacci number. Running the above example would produce the following output, in which we can see the 40th Fibonacci number is 102334155 and how the dictionary MEMO is used to store different calls to the function.



This post first appeared on Best Tech 247 Microsoft Silver Certified Partner, please read the originial post: here

Share the post

A Light Introduction to Decorators in Python

×

Subscribe to Best Tech 247 Microsoft Silver Certified Partner

Get updates delivered right to your inbox!

Thank you for your subscription

×