Decorators: Put a Pythonista Spin on Your Function Decoration

Hello readers!

Welcome to my blog (or my journal, if you argue otherwise). Today, I will officially be writing my first fully technical article as a Kiboan. Though this introduction is not overly important, I must start my conversation with a gentle piece—ambiguity intended.

Decoration is a weapon. When this weapon lands softly in the hands of a skilled human, it transforms into a creative masterpiece (whether excruciating or blissful). A cosmetologist uses this weapon to create beauty, like an assassin's blood. No wonder movie casts mutate into various forms during various scenes at the touch of a makeup artist who has been weaponized by this weapon (pun intended).

To decorate means to make something look more attractive by adding extra items or images to it.

Now the question arises:

What on earth would compel a programmer to decorate his/her function?

A programmer cherishes clean code, but this comes at a price. He moves from writing structurally-oriented programs to writing functionally-oriented programs. What a big leap in his approach, and still he is unsatisfied. He needs something more to make him the fortune of perfection he seeks. Then he learns...

Decorators! And his life never remained the same! So, what about you?

Introduction to Decorators

What is a decorator?

A decorator is:

  1. A design pattern that allows the addition of new functionality to an existing object without modifying its internals.

  2. A function that takes another function and extends its behavior/functionality without explicitly modifying it.

According to the definitions above, a decorator is simply:

  1. Accepts another function as an argument.

  2. Modifies or enhances the function.

  3. Returns the modified function.

In Python, everything is an object. Hence, functions are first class citizens.

Let's see how to implement this decorator pattern in Python:

First, you create the function you want to extend.

from typing import Literal
import string


def calc_freq(text: str, type: Literal['w', 'c']) -> dict:
  """
  Calculate the frequencies of words or characters.

  :param text: (str) The text to be analysed
  :param type: (str) must be either 'w' (word frequencies) or
  'c' (character frequencies)

  :return: dict
  """
  frequencies = {}
  splitter = {
    'w': text.split(),
    'c': [c for c in text if c not in string.whitespace]
  } # splitted text based on type given

  if type not in splitter.keys():
    raise ValueError('Error: Type must be either "w" or "c"!')

  # loop through splitted text and calculate frequency
  for item in splitter[type]:
    item = item.lower()
    frequencies[item] = frequencies.get(item, 0) + 1

  return frequencies

Go through the code above and try to understand what it does.

It calculates the frequencies of words or characters based on the type provided. We can assume that it would take more time to calculate character frequencies than it would to calculate word frequencies. But to prove this, let us write a decorator function that tracks the execution time of the function.

Note that logging is one of the use cases for function decorators.

To do this, we will create another function that accepts our function as an argument.

def get_execution_time(func):
    pass

Remember, a decorator modifies the function passed to it as an argument and returns the modified function. To modify a function within a function decorator without compromising its internal implementation, you must create another function within the function decorator. This inner function would add functionality to the function passed as an argument, and this function can then be returned.

def get_execution_time(func):
    def inner():
        pass
    return inner

The above code snippet shows a skeletal description of what a decorator looks like. Let's complete the function decorator...

from time import time


def get_exec_time(func):
    def wrapper(text, type_):
        start_time = time()
        result = func(text, type_)
        end_time = time()
        print(f'Elapsed time: {end_time - start_time}')
        return result

    return wrapper

To call this decorator on the function, we use the @ symbol, like this:

@get_exec_time
def calc_freq(text: str, type: Literal['w', 'c']) -> dict:
    # your code here

The decorator will be invoked on the function when you call calc_freq. View the full code on Replit.

Try modifying the function decorator to output the execution time of each function call to a file. Remember to start with passing an argument to the decorator (aside from the function passed as an argument).

Another use case of decorators is Authentication. You can restrict a user from accessing certain features provided via a function until they are authenticated.

How do you aim to use decorators in your next project?

AN ABRUPT BYE!...

References

  1. Datacamp - What is a decorator?

  2. Real Python - Primer on Python Decorators

  3. Python 101 - Decorators

Yes, I made this article as short as possible and avoided covering more information on Decorators because I hope to make this an introduction to the topic. To learn more about decorators, check out the references provided. You could also explore some of the built-in decorators used in Python during class creation such as property, classmethod, and staticmethod.