avatarWouter van Heeswijk, PhD

Summary

This article provides a concise introduction to Python wrappers and decorators, explaining their structure and providing examples on how they can be used to write more maintainable and modular code.

Abstract

Python wrappers and decorators are functionalities that enhance code readability and reusability. Wrappers are functions or classes that encapsulate the behavior of another function, providing a layer around the original functionality, allowing to modify or extend behavior without altering the core implementation. Decorators are a clean and readable way to wrap functions, using the '@decorator' syntax to apply the wrapper to a specific function. Wrappers are used when we want to modify or extend the behavior of an existing function, method, or class without modifying its source code. Use cases for wrappers include adding functionality, modifying behavior, maintaining consistency, and controlling security.

Bullet points

  • Wrappers and decorators are functionalities that enhance code readability and reusability in Python.
  • Wrappers are functions or classes that encapsulate the behavior of another function, providing a layer around the original functionality.
  • Decorators are a clean and readable way to wrap functions, using the '@decorator' syntax to apply the wrapper to a specific function.
  • Wrappers are used when we want to modify or extend the behavior of an existing function, method, or class without modifying its source code.
  • Use cases for wrappers include adding functionality, modifying behavior, maintaining consistency, and controlling security.
  • Wrappers can be employed to add extra functionalities to a function, such as logging, timing, or error handling.
  • Wrappers can be used to alter the behavior of a function, for instance transforming output data types or modifying behavior based on external conditions.
  • Wrappers can be employed to ensure consistency across multiple functions, such as standardizing input and output formats, applying uniform error handling, or employing validation logic.
  • Security features, such as access control or data encryption, can be introduced using wrappers.
  • Wrappers and decorators are essential tools in a Python developer’s toolkit.

Unwrapping the Power of Python: A Quick Guide to Wrappers and Decorators

Extend or modify the behavior of your functions without altering source code

Photo by Olena Sergienko on Unsplash

The popularity of Python programming language can be largely explained due to its facilitation of clean, concise, and efficient code. Wrappers and decorators are among the functionalities that enhance code readability and reusability in Python. In this article we offer a concise introduction to these concepts, explaining their structure and providing examples on how they can be used to write more maintainable and modular code.

What are wrappers?

Photo by freestocks on Unsplash

Wrappers in Python are functions or classes that encapsulate — or ‘wrap’, as you will — the behavior of another function. They provide a layer around the original functionality, allowing to modify or extend behavior without altering the core implementation. Typical use cases of wrappers are tasks such as logging, timing, or error handling.

Let’s consider a simple example of a wrapper function, showcasing it’s general structure. This example does not have any meaningful functionality yet, but the setup is similar to that of most wrappers:

Note we take an original function (print_function) and wrap it with our example_wrapper to create a new function wrapped_print_function with new functionality (recall that functions are also objects in Python). The output of routine is as follows, take some time to familiarize yourself with what lines are called where:

‘example_wrapper’ has been called Moving to ‘wrapper’ now ‘wrapper’ before function execution Arguments: Hello world! Keyword arguments: Inside ‘print_function’ now Hello world! ‘wrapper’ after function execution

Effectively, the example_wrapper takes the function ‘print_function’ as input, extends its behavior withwrapper, and then outputs the wrapper as the extended function that encapsulates the original one.

The two-layer structure is common for wrappers, but might need some more detail:

  1. Outer Layer (example_wrapper)
  • The outer layer is the actual wrapper function. It takes another function (func) as its argument.
  • Its purpose is to define and return the inner layer (wrapper) function.
  • This outer layer can perform setup, configuration, or any other logic that needs to be done once when applying the decorator.

2. Inner Layer (wrapper)

  • The inner layer is the wrapper function that is returned by the outer layer. It is the actual function that wraps around the original function (func).
  • Its purpose is to add or modify behavior before and/or after calling the original function.
  • The inner layer often calls the original function (func) somewhere within its body.
  • Defining the inputs *arg and **kwarg enables accessing all arguments of the input function. Note this is possible because wrapper is contained in example_wrapper , which in turn has func as its input.
  • The layer can access the arguments and results of the original function and perform additional actions.

We performed the wrapping by creating a new function via wrapped_print_function = example_wrapper(print_function). In principle, we can even do this with multiple wrappers, e.g., one than tracks execution time and one that prints inputs and outputs. This could get a bit messy. Fortunately, there is a cleaner and more Pythonic way to do it. Enter the decorators.

What are decorators?

Photo by Annie Spratt on Unsplash

Creating explicit wrapped functions and calling them instead of the originals is often not ideal, especially when only used for testing or developing purposes. For instance, we might want to know how much time a function takes, without extending that behavior into the production version.

Python offers a clean and readable way to wrap functions, using the ‘@decorator’ syntax to apply the wrapper to a specific function (and can be removed just as easily). These kinds of decorators are extensively used in Python, and you may have come across some of them already, e.g., frozen dataclasses @dataclass(frozen=True) .

There are multiple definitions of wrappers, decorators and adapters. In this article, I use the term ‘wrapper’ to describe the functionality and the term ‘decorator’ for the ‘@decorator’ . There is more nuance to the discussion though.

Let’s revisit the example before, performing the exact same operations with the decorator syntax:

The output is identical, but the code is cleaner and more concise. Note that we added @example_wrapper to decorate the original print function, and then simply call the print function as we usually would. If the wrapper ever loses its purpose, we can simply remove the decorator.

Note it is possible to stack multiple decorators, for instance:

In this example, print_function is first wrapped with the `timing_wrapper and then further wrapped with the log_wrapper. Thus, the top wrapper is applied as the outermost wrapper. Much easier than creating multiple explicitly wrapped functions, right?

When working with more extensive stacks of decorators, it is important to keep in mind the ordering of decorators, as well as potential interactions between them. Finally, remind that each decorator introduces some overhead; routinely running many decorators may substantially slow down your code.

Enough theory, what can we actually do with wrappers?

When do we use wrappers and decorators?

We use wrappers in Python when we want to modify or extend the behavior of an existing function, method, or class without modifying its source code. There are several use cases for wrappers, including:

Adding functionality

Wrappers can be employed to add extra functionalities to a function, such as logging, timing, or error handling.

For instance, we may create a wrapper to log input and output parameters of a function. For this, we simply access its *args and **kwargs arguments.

One more? We are often interested in how much time a function takes to execute. In a wrapper, we can simply measure time before and after running the function.

Modifying behavior

Wrappers are useful for altering the behavior of a function, for instance transforming output data types or modifying behavior based on external conditions. As an example, below we transform output into a string:

Maintaining consistency

Wrappers can be employed to ensure consistency across multiple functions. This can be achieved, for instance, by standardizing input and output formats, applying uniform error handling, or employing validation logic. The example below standardizes all input arguments as strings.

Controlling security

Security features, such as access control or data encryption, can be introduced using wrappers. For instance, a wrapper may be designed to check user permissions before executing a function or encrypt sensitive data before passing it to a function. Below, the wrapper verifies whether the user has the "execute" permission.

Wrapping up (pun intended)

Wrappers and decorators are essential tools in a Python developer’s toolkit. They provide a clean and elegant way to modify or extend the behavior of functions, without cluttering the core implementation. Wrappers take the original function as input and can, e.g., perform additional operations on its input and output. The decorator syntax enables to easily adjust the behavior of your original function by adding a single line an top of it. There is a wide array of existing wrappers that can readily be employed on your functions.

Happy wrapping!

Photo by Juliana Malta on Unsplash

References

Geeks for Geeks (2020). Function Wrappers in Python. https://www.geeksforgeeks.org/function-wrappers-in-python/

Refactoring Guru (n.d). Decorator. https://refactoring.guru/design-patterns/decorator

Wrapper
Decorators
Python Programming
Modular
Data Science
Recommended from ReadMedium