Introduction
In this lesson, we will delve into the advanced concept of decorators in Python. Decorators are design patterns that allow the modification of functions or methods using other functions. This lesson builds on your understanding of higher-order functions by showcasing how decorators can alter the functionality of existing code without modifying the original function's structure. We will explore fundamental decorator syntax, using the @decorator notation, learn how to write decorators that accept arguments, and ensure we preserve a decorated function's metadata with functools.wraps. Additionally, we will examine the use of multiple decorators and their order of execution.
How Decorators Work
Decorators are essentially higher-order functions that take another function as an argument and return a new function with enhanced features. Conceptually, a decorator wraps a function and adds new behavior to it without changing its source code.
Basic Decorator Implementation
To implement a basic decorator, we define a function that returns a wrapper function. The @decorator syntax is a Pythonic shorthand for applying decorators.
def my_decorator(func):
def wrapper():
print("Something happens before the function.")
func()
print("Something happens after the function.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
// Output:
Something happens before the function.
Hello!
Something happens after the function.
Decorators with Arguments
Sometimes, decorators need to accept arguments. This is achieved by nesting an additional function level.
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
func(*args, **kwargs)
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
// Output:
Hello, Alice!
Hello, Alice!
Hello, Alice!
Preserving Function Metadata
When decorators modify functions, the original function's metadata (name, docstring, etc.) may be lost. To avoid this, use the functools.wraps decorator from the functools module.
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Decorator is called.")
return func(*args, **kwargs)
return wrapper
@my_decorator
def example_func():
"""This is an example function."""
print("Example function is running.")
print(example_func.__name__)
print(example_func.__doc__)
// Output:
example_func
This is an example function.
Applying Multiple Decorators
When using multiple decorators, they are applied from the innermost to the outermost. This order of application can affect the result.
def bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def text():
return "Decorated Text"
print(text())
// Output:
<b><i>Decorated Text</i></b>
Summary / Key Takeaways
- Decorators wrap functions to modify their behavior without altering their structure, enhancing code modularity.
- Basic decorators can be implemented using function definitions and the
@decoratorsyntax, which improves readability and reduces boilerplate. - Argument-supporting decorators utilize nested functions, allowing for dynamic decorator configurations.
- Metadata preservation using
functools.wrapsensures the original function's name and documentation remain intact after decoration. - Multiple decorators apply in a nested fashion, from the innermost outward, impacting final functionality and composition.
By mastering decorators, you can build flexible, clean, and maintainable Python applications, leveraging the power of function modification with minimal code redundancy.
