Master the art of using decorators to modify and enhance Python functions with clean, reusable code.
A decorator is a function that takes another function as an argument and returns a modified version of that function.
Decorators allow you to modify the behavior of a function without changing its source code, following the principle of separation of concerns.
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
# This is equivalent to:
# say_hello = my_decorator(say_hello)
say_hello()
# Output:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.
Decorators can accept arguments that modify their behavior, allowing for more flexible and reusable decorator functions.
When a decorator needs to accept arguments, it requires an additional level of function nesting to handle the parameters.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
def validate_type(expected_type):
def decorator(func):
def wrapper(*args, **kwargs):
for arg in args:
if not isinstance(arg, expected_type):
raise TypeError(f"Expected {expected_type.__name__}, got {type(arg).__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_type(int)
def add(a, b):
return a + b
Class decorators are decorators that modify or enhance the behavior of entire classes rather than individual methods.
Class decorators can add new methods, modify existing ones, or add class-level attributes to the decorated class.
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
print("Initializing database connection...")
def query(self, sql):
print(f"Executing query: {sql}")
# Both db1 and db2 will be the same instance
db1 = Database()
db2 = Database()
print(db1 is db2) # Output: True
def add_methods(cls):
def new_method(self):
return "This is a new method"
cls.new_method = new_method
return cls
@add_methods
class MyClass:
pass
obj = MyClass()
print(obj.new_method()) # Output: This is a new method
Method decorators are decorators that modify the behavior of class methods, including instance methods, class methods, and static methods.
Method decorators can be used to add functionality to class methods, such as access control, logging, or validation.
def log_method_call(func):
def wrapper(self, *args, **kwargs):
print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(self, *args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
class Calculator:
@log_method_call
def add(self, x, y):
return x + y
@log_method_call
def multiply(self, x, y):
return x * y
calc = Calculator()
calc.add(5, 3) # Output: Calling add with args: (5, 3), kwargs: {}
# Output: add returned: 8
calc.multiply(4, 2) # Output: Calling multiply with args: (4, 2), kwargs: {}
# Output: multiply returned: 8
The @property decorator allows you to define methods that can be accessed like attributes, providing a way to implement getters, setters, and deleters.
Property decorators enable you to add validation, computation, or other logic to attribute access while maintaining a clean interface.
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError("Name must be a string")
self._name = value
@name.deleter
def name(self):
del self._name
person = Person("Alice")
print(person.name) # Output: Alice
person.name = "Bob"
print(person.name) # Output: Bob
# This will raise a TypeError
# person.name = 123
del person.name
# This will raise an AttributeError
# print(person.name)
Python provides several built-in decorators that serve common purposes, such as @staticmethod, @classmethod, and @functools.wraps.
These decorators are part of Python's standard library and provide functionality that is commonly needed in Python programs.
from functools import wraps
def my_decorator(func):
@wraps(func) # Preserves the metadata of the original function
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Math:
@staticmethod
def add(x, y):
return x + y
@classmethod
def from_string(cls, string):
x, y = map(int, string.split(','))
return cls(x, y)
def __init__(self, x, y):
self.x = x
self.y = y
# Using static method
print(Math.add(5, 3)) # Output: 8
# Using class method
math_obj = Math.from_string("10,20")
print(math_obj.x, math_obj.y) # Output: 10 20
Decorator chaining allows you to apply multiple decorators to a single function, with each decorator adding its own functionality.
When multiple decorators are applied, they are executed from bottom to top, with the innermost decorator being applied first.
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def validate_args(func):
def wrapper(*args, **kwargs):
if not all(isinstance(arg, int) for arg in args):
raise TypeError("All arguments must be integers")
return func(*args, **kwargs)
return wrapper
@log_calls
@validate_args
def add(a, b):
return a + b
# This is equivalent to:
# add = log_calls(validate_args(add))
print(add(5, 3)) # Output: Calling add
# Output: 8
# This will raise a TypeError
# print(add("5", 3))
A decorator factory is a function that returns a decorator, allowing for more flexible and reusable decorator creation.
Decorator factories enable you to create decorators with different behaviors based on parameters passed to the factory function.
def retry(max_attempts=3, delay=1):
def decorator(func):
def wrapper(*args, **kwargs):
import time
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise e
print(f"Attempt {attempts} failed, retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=2)
def unreliable_function():
import random
if random.random() < 0.7:
raise ValueError("Random error")
return "Success!"
# This will retry up to 3 times with a 2-second delay between attempts
print(unreliable_function())
The @contextmanager decorator from the contextlib module allows you to create context managers using generator functions.
Context manager decorators simplify the creation of context managers by handling the setup and teardown logic in a single function.
from contextlib import contextmanager
@contextmanager
def temporary_file():
import tempfile
import os
# Setup
temp = tempfile.NamedTemporaryFile(delete=False)
try:
yield temp
finally:
# Teardown
temp.close()
os.unlink(temp.name)
# Using the context manager
with temporary_file() as f:
f.write(b"Hello, World!")
f.flush()
print(f"File written to: {f.name}")
# File is automatically deleted after the with block
Best practices for writing and using decorators effectively in Python code.
Following these guidelines helps ensure your decorators are maintainable, reusable, and don't introduce unexpected behavior.
from functools import wraps
# Good: Preserving function metadata
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
# Bad: Not preserving metadata
def bad_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
# Good: Using decorator factories for flexibility
def retry(max_attempts=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Implementation
pass
return wrapper
return decorator
# Good: Documenting decorator behavior
def documented_decorator(func):
"""Decorator that logs function calls and their results."""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper