Contents

Python Interactive Guide - Step 3 Functions (9) - Decorators

Series - Python Interactive Guide
Info
  • This course, Python Interactive Guide, is designed to help you learn the basics of Python programming through hands-on, interactive examples.
  • The “Style Guide” sections introduce clean coding practices, mainly based on PEP8.
  • You can run and experiment with every code example.
    Feel free to try things out - reloading the page will reset everything.

This is a continuation of “Step 3 Functions”.

In the previous section, we learned about “higher-order functions” that handle functions as inputs or outputs.
In this section, we’ll explore “decorators,” which are an application of higher-order functions.

Decorators are mechanisms for modifying the behavior of existing functions.
As the name suggests, they can be thought of as “decorating” functions.

The Role of Decorators

Decorators enable you to:

  • Add processing before and after function execution
  • Validate or transform function arguments and return values
  • Modify function behavior (such as adding memoization capabilities)

A decorator is essentially a function that takes a function as an argument and returns a new function.
Passing a function to a decorator returns a decorated function.
Therefore, decorators are a type of higher-order function that we covered in the previous section.

Let’s look at a simple decorator:

In this code, the following process takes place:

  1. The decorator my_decorator takes a function func as an argument and returns a new function wrapper
  2. The wrapper function adds additional processing before and after executing the original function func
  3. When we apply the decorator to the say_hello function, we get a decorated new function
  4. When that function is called, it executes the original function’s processing along with the additional processes before and after

The function returned by the decorator calls the original function internally while incorporating additional processing before and after. Because it “wraps” the original function, it’s commonly called a wrapper function.

In Python, using decorators allows you to flexibly add new processing to existing functions.

📚Exercise

Create a decorator name_printer that displays the function name before execution.
Note that for a function func, its name can be obtained with func.__name__.

Sample Solution

Python provides a simple syntax for applying decorators to functions.

Decorator Syntax
@decorator_name def function_name(): ...
  • Add @<decorator_name> above the definition of the function you want to decorate.

The previous example can be rewritten as follows:

Adding @my_decorator to the function say_hello is equivalent to say_hello = my_decorator(say_hello).
Using this decorator syntax makes function decoration more concise.

📚Exercise

Rewrite the previous name_printer decorator program using decorator syntax.

Sample Solution

You can also apply multiple decorators to a function:

Note that multiple decorators are applied from bottom to top. In other words, the decorator closest to the function definition is applied first, followed by the decorator above it.

@decorator1
Outer decorator (applied last)
@decorator2
Inner decorator (applied first)
say_hello body
print("Hello!")

In the examples so far, we’ve only applied decorators to simple functions without parameters or return values.
The following implementation creates a generic decorator that can be applied to any function:

The flow of this code is as follows:

  1. The decorator my_decorator takes a function func and returns a new function wrapper
  2. The wrapper function has the following roles:
    1. Use variable-length arguments (args, kwargs parameters) to accept arguments for any function
    2. Add additional processing before and after executing the original function func
    3. Pass the received arguments to func and store the result in result
    4. Return result as the return value
  3. When a decorator is applied to a function, a decorated new function is obtained
  4. When that function is called, it executes the original function’s processing with additional processes before and after, and returns the same return value as the original function

By using variable-length arguments, you can create decorators applicable to any function regardless of its arguments.

📚Exercise

Create a decorator measure_time that measures the execution time (in seconds) of any function.

Use the following code as a reference for getting the execution time:

Sample Solution

Since decorators are functions and can be treated as objects, we can also create functions that return decorators:

In this code, the following happens:

  1. Executing repeat(n=3) returns the decorator function
  2. The returned decorator function is applied to the say_hello function
  3. As a result, when the say_hello function is called, the original function is executed 3 times internally
📚Exercise

Extend the previous measure_time decorator by creating a function measure_avg_time that takes a trials option and returns a decorator which runs the function the specified number of times and shows the average execution time.

Sample Solution

One issue when using decorators is that the decorated function loses the metadata (name, docstring, parameter information, etc.) of the original function. This can be problematic for debugging and generating documentation.

This issue can be solved using the functools.wraps function:

functools.wraps is a type of “function that returns a decorator” as discussed in the previous subsection.
It takes a function and returns a decorator that preserves the metadata (name, docstring, parameter information, etc.) of the decorated function.

By applying this to the wrapper function, the metadata of the original function is preserved even when using decorators, making debugging and documentation generation easier.

📚Exercise

Use functools.wraps with the measure_time decorator from the previous Exercise to ensure the function information is preserved.

Sample Solution

In this section, we learned the following points about Python decorators:

  1. What are Decorators: Mechanisms for changing the behavior of existing functions (a type of higher-order function)
  2. Decorator Syntax: Apply decorators using the concise @decorator_name syntax
  3. Generic Decorators: How to create decorators applicable to any function
  4. Functions That Return Decorators: How to create decorators that accept parameters
  5. Using functools.wraps: How to preserve the metadata of the original function when using decorators

Decorators are one of Python’s powerful features, allowing you to easily add logging, execution time measurement, input validation, and other processes. By implementing these additional processes independently from the original process, your code becomes more organized and reusable.

Next time, we’ll learn about “generator functions,” which are important for efficiently processing large amounts of data.

Related Content