- β¨ Python Decorators
- π― What Are Decorators?
- π Creating a Simple Decorator
- π¦ Using Multiple Decorators
- π§βπ« Decorators with Arguments
- 𧩠Built-in Decorators:
@staticmethod
,@classmethod
, and@property
- β¨ Advanced Decorators:
functools.wraps
- π Common Use Cases
- π Best Practices for Writing Decorators
- π― Key Takeaways
A decorator is a function in Python that modifies the behavior of another function or class. Decorators allow you to wrap another function in order to extend or alter its behavior, without modifying the function itself.
They are often used for logging, enforcing access control, instrumentation, or modifying inputs/outputs of a function.
In Python, functions are first-class citizens, meaning they can be passed around as arguments to other functions. Decorators take advantage of this feature.
A decorator is applied using the @decorator_name
syntax before the definition of the function you want to decorate.
@decorator_name
def some_function():
pass
This is equivalent to:
def some_function():
pass
some_function = decorator_name(some_function)
Letβs create a basic decorator that prints the execution time of a function:
import time
def time_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function {func.__name__} executed in {end_time - start_time:.4f} seconds")
return result
return wrapper
Now, we can use this decorator:
@time_decorator
def calculate_square(n):
return n ** 2
print(calculate_square(5))
wrapper
function: The decorator wraps the original function inside another function (wrapper
), adding additional logic (in this case, measuring execution time). Closure allows thewrapper
function to access the original function's arguments and return value.*args, **kwargs
: These ensure that the wrapper function can accept any number of arguments and keyword arguments, just like the original function.
You can apply multiple decorators to a single function. The decorators are applied from the top down:
@decorator_one
@decorator_two
def my_function():
pass
This is equivalent to:
my_function = decorator_one(decorator_two(my_function))
Sometimes you want your decorator to accept arguments. To achieve this, you need to add another layer of function nesting:
def repeat_decorator(n_times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
Now you can control how many times a function is executed:
@repeat_decorator(3)
def greet():
print("Hello!")
greet()
repeat_decorator(3)
: This creates the decorator that will repeat the function three times.- The decorator returns the actual wrapper, which handles the execution.
Python provides several built-in decorators to simplify common tasks, especially in object-oriented programming:
Declares a method that does not access or modify the class or instance state.
class Math:
@staticmethod
def add(a, b):
return a + b
# You can call it directly from the class
print(Math.add(2, 3)) # Output: 5
When you want to group functions together in a class but they don't need access to the class variables or instance
Declares a method that takes the class as its first argument (cls
).
class Animal:
species_count = 0
def __init__(self, name):
self.name = name
Animal.species_count += 1
@classmethod
def total_species(cls):
return f"Total species: {cls.species_count}"
# Creating instances
dog = Animal("Dog")
cat = Animal("Cat")
# Call class method
print(Animal.total_species()) # Output: Total species: 2
When you need to work with class-level variables or methods that should affect the class itself, not the instance
Allows you to define methods that can be accessed like attributes.
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def area(self):
return self._width * self._height
rect = Rectangle(5, 10)
print(rect.area) # Output: 50
you can also define a setter and a deleter methods using @property.setter
and @property.deleter
.
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value > 0:
self._width = value
else:
raise ValueError("Width must be positive!")
rect = Rectangle(5, 10)
rect.width = 8 # This calls the setter
print(rect.width) # Output: 8
rect.width = -2 # This raises a ValueError
When writing decorators, you may notice that the metadata (like the name and docstring) of the original function is lost. This is where functools.wraps
comes in. It preserves the original function's metadata:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Calling function...")
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
"""This is my function."""
pass
print(my_function.__name__) # Output: my_functions
By applying @wraps(func)
in the wrapper
, the original functionβs name, docstring, and other attributes are preserved.
Decorators are widely used for various purposes, including:
- Logging: Track when functions are called and with what arguments.
- Authorization: Restrict access to certain parts of an application.
- Caching: Store the results of expensive function calls to avoid repeated computation.
- Rate Limiting: Control the frequency at which a function can be called.
For example, hereβs a logging decorator:
def log_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Function {func.__name__} was called with {args} and {kwargs}")
return func(*args, **kwargs)
return wrapper
- Use
@wraps
: Always usefunctools.wraps
to maintain the original functionβs metadata. - Handle Function Signature Properly: Ensure the decorator supports any kind of function signature using
*args
and**kwargs
. - Keep Them Reusable: Write decorators in a way that they can be used across different parts of your codebase.
- Decorators enhance or modify the behavior of functions or methods.
- They are extremely useful for cross-cutting concerns like logging, authorization, and caching.
- Use
functools.wraps
to ensure your decorator doesn't overwrite important metadata of the decorated function.
β¬ οΈ Previous: closures
β‘οΈ Next: Lists
π Back to Top
π Home