Python Interactive Guide - Step 3 Functions (9) - Decorators
- 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”.
3.8. Decorators
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.
3.8.1. What are Decorators?
Decorators are mechanisms for modifying the behavior of existing functions.
As the name suggests, they can be thought of as “decorating” functions.
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)
How Decorators Work
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:
- The decorator
my_decorator
takes a functionfunc
as an argument and returns a new functionwrapper
- The
wrapper
function adds additional processing before and after executing the original functionfunc
- When we apply the decorator to the
say_hello
function, we get a decorated new function - 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.
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
3.8.2. Decorator Syntax
Python provides a simple syntax for applying decorators to functions.
- 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.
Rewrite the previous name_printer
decorator program using decorator syntax.
Sample Solution
Applying Multiple Decorators
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.
Outer decorator (applied last)
Inner decorator (applied first)
print("Hello!")
3.8.3. Generic Decorators
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:
- The decorator
my_decorator
takes a functionfunc
and returns a new functionwrapper
- The
wrapper
function has the following roles:- Use variable-length arguments (
args
,kwargs
parameters) to accept arguments for any function - Add additional processing before and after executing the original function
func
- Pass the received arguments to
func
and store the result inresult
- Return
result
as the return value
- Use variable-length arguments (
- When a decorator is applied to a function, a decorated new function is obtained
- 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.
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
3.8.4. Functions That Return Decorators
Since decorators are functions and can be treated as objects, we can also create functions that return decorators:
In this code, the following happens:
- Executing
repeat(n=3)
returns thedecorator
function - The returned
decorator
function is applied to thesay_hello
function - As a result, when the
say_hello
function is called, the original function is executed 3 times internally
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
3.8.5. Using functools.wraps
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.
Use functools.wraps
with the measure_time
decorator from the previous Exercise to ensure the function information is preserved.
Sample Solution
Summary
In this section, we learned the following points about Python decorators:
- What are Decorators: Mechanisms for changing the behavior of existing functions (a type of higher-order function)
- Decorator Syntax: Apply decorators using the concise
@decorator_name
syntax - Generic Decorators: How to create decorators applicable to any function
- Functions That Return Decorators: How to create decorators that accept parameters
- 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.