This tutorial will teach you everything you need to know about Python decorators. You’ll learn how to create, use, and combine decorators to enhance your Python code. By the end, you’ll be able to apply decorators in real-world scenarios and solve complex problems with ease.
Step 1: Understanding Functions as First-Class Objects
In Python, functions are first-class objects, meaning they can be passed as arguments, returned from other functions, and assigned to variables. This is the foundation of decorators.
- Key Concepts:
- Functions can be assigned to variables.
- Functions can be passed as arguments to other functions.
- Functions can return other functions.
- Why It Matters: Decorators rely on these properties to modify or extend the behavior of functions.
def greet(name):
return f'Hello, {name}!'
# Assigning a function to a variable
my_greet = greet
print(my_greet('Alice')) # Output: Hello, Alice!Practice Exercise
Create a function multiply(a, b) that returns the product of two numbers. Assign this function to a variable my_multiply and call it with the values 5 and 3. What is the output?
Show Solution
def multiply(a, b):
return a * b
my_multiply = multiply
print(my_multiply(5, 3)) # Output: 15Step 2: Creating a Simple Decorator
A decorator is a function that takes another function as input and returns a new function with added behavior.
- Steps to Create a Decorator:
1. Define a decorator function that accepts a function as an argument.
2. Define a wrapper function inside the decorator to add new behavior.
3. Return the wrapper function.
- Example: A decorator that logs the execution of a function.
def log_execution(func):
def wrapper(*args, **kwargs):
print(f'Executing {func.__name__}')
return func(*args, **kwargs)
return wrapper
@log_execution
def greet(name):
return f'Hello, {name}!'
print(greet('Bob')) # Output: Executing greet
# Hello, Bob!Practice Exercise
Create a decorator uppercase_result that converts the result of a function to uppercase. Apply it to a function greet(name) that returns 'Hello, {name}!'. What happens when you call greet('Alice')?
Show Solution
def uppercase_result(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase_result
def greet(name):
return f'Hello, {name}!'
print(greet('Alice')) # Output: HELLO, ALICE!Step 3: Decorators with Arguments
Sometimes, you need decorators that accept arguments. To achieve this, you need an additional layer of nesting.
- Steps:
1. Define a decorator factory function that accepts arguments.
2. Inside the factory, define the decorator function.
3. Return the decorator function.
- Example: A decorator that repeats a function's execution a specified number of times.
def repeat(num_times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f'Hello, {name}!')
greet('Charlie') # Output: Hello, Charlie!
# Hello, Charlie!
# Hello, Charlie!Practice Exercise
Create a decorator delay(seconds) that delays the execution of a function by a specified number of seconds. Use the time.sleep() function. Apply it to a function say_hello() that prints 'Hello!'. What happens when you call say_hello() with a delay of 2 seconds?
Show Solution
import time
def delay(seconds):
def decorator(func):
def wrapper(*args, **kwargs):
time.sleep(seconds)
return func(*args, **kwargs)
return wrapper
return decorator
@delay(2)
def say_hello():
print('Hello!')
say_hello() # Output: (After 2 seconds) Hello!Step 4: Chaining Decorators
You can apply multiple decorators to a single function. The decorators are applied from the bottom up.
- Example: Combining log_execution and uppercase_result decorators.
@log_execution
@uppercase_result
def greet(name):
return f'Hello, {name}!'
print(greet('Dave')) # Output: Executing wrapper
# HELLO, DAVE!Practice Exercise
Create two decorators: add_prefix(prefix) that adds a prefix to the result of a function, and add_suffix(suffix) that adds a suffix. Apply both decorators to a function greet(name) that returns 'Hello, {name}!'. What happens when you call greet('Eve') with a prefix of '>>> ' and a suffix of ' <<<'?
Show Solution
def add_prefix(prefix):
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return prefix + result
return wrapper
return decorator
def add_suffix(suffix):
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + suffix
return wrapper
return decorator
@add_prefix('>>> ')
@add_suffix(' <<Step 5: Using `functools.wraps` to Preserve Metadata
When using decorators, the original function's metadata (e.g., __name__, __doc__) is lost. The functools.wraps decorator preserves this metadata.
- Example: Using wraps to maintain the original function's name.
from functools import wraps
def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f'Executing {func.__name__}')
return func(*args, **kwargs)
return wrapper
@log_execution
def greet(name):
return f'Hello, {name}!'
print(greet.__name__) # Output: greetPractice Exercise
Create a decorator timing that measures the execution time of a function using time.time(). Use functools.wraps to preserve the original function's metadata. Apply it to a function calculate_sum(n) that calculates the sum of numbers from 1 to n. What is the execution time for calculate_sum(1000000)?
Show Solution
import time
from functools import wraps
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'{func.__name__} took {end_time - start_time:.4f} seconds')
return result
return wrapper
@timing
def calculate_sum(n):
return sum(range(1, n + 1))
calculate_sum(1000000) # Output: calculate_sum took X.XXXX secondsStep 6: Real-World Use Case: Caching with Decorators
Decorators are often used for caching expensive function calls. The functools.lru_cache decorator is a built-in solution, but you can also create your own.
- Example: A custom caching decorator.
def cache(func):
cached_results = {}
def wrapper(*args):
if args in cached_results:
return cached_results[args]
result = func(*args)
cached_results[args] = result
return result
return wrapper
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # Output: 55Practice Exercise
Create a decorator memoize that caches the results of a function based on its arguments. Apply it to a recursive function factorial(n) that calculates the factorial of n. What is the result of factorial(10)?
Show Solution
def memoize(func):
cache = {}
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize
def factorial(n):
if n == 0 or n == 1:
return 1
return n * factorial(n - 1)
print(factorial(10)) # Output: 3628800Step 7: Advanced: Class-Based Decorators
Decorators can also be implemented as classes. This is useful when you need to maintain state or provide additional functionality.
- Steps:
1. Define a class with a __call__ method.
2. Use the class instance as a decorator.
- Example: A class-based decorator that counts function calls.
class CallCounter:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f'{self.func.__name__} has been called {self.count} times')
return self.func(*args, **kwargs)
@CallCounter
def greet(name):
return f'Hello, {name}!'
print(greet('Frank')) # Output: greet has been called 1 times
# Hello, Frank!Practice Exercise
Create a class-based decorator Timer that measures the execution time of a function. Use the time.time() function. Apply it to a function calculate_product(n) that calculates the product of numbers from 1 to n. What is the execution time for calculate_product(1000)?
Show Solution
import time
class Timer:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
start_time = time.time()
result = self.func(*args, **kwargs)
end_time = time.time()
print(f'{self.func.__name__} took {end_time - start_time:.4f} seconds')
return result
@Timer
def calculate_product(n):
product = 1
for i in range(1, n + 1):
product *= i
return product
calculate_product(1000) # Output: calculate_product took X.XXXX secondsSign in to take Cornell notes on this lesson — they save automatically and stay with your account.