Decorators
Python decorators are a very powerful and useful tool that allows programmers to modify the behavior of a function or class. Decorators allow for the extension or modification of the function's behavior without permanently modifying it. Here’s how to use and create decorators in Python:
Understanding Decorators
Before diving into creating decorators, it's essential to understand that functions in Python are first-class objects. This means that they can be passed around and used as arguments, just like any other object (strings, integers, etc.).
Simple Decorator
Here's a simple example of a decorator that adds a welcoming message to the function it decorates:
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!")
say_hello()
Using the functools.wraps Decorator
When you use a decorator, you're replacing one function with another. This can make debugging harder because the function that's being called isn't the one you defined but rather the wrapper function. To make debugging easier, use the functools.wraps decorator in your own decorators:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Something is happening before the function is called.")
result = func(*args, **kwargs)
print("Something is happening after the function is called.")
return result
return wrapper
Decorators with Arguments
Sometimes you might want to pass arguments to your decorator. For instance, a decorator that repeats the execution of a function a given number of times could be implemented as follows:
from functools import wraps
def repeat(num_times):
def decorator_repeat(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello {name}")
greet("Alice")
Class-based Decorators
Decorators can also be implemented as classes. Here's an example of a class-based decorator that behaves similarly to the first example:
class MyDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Something is happening before the function is called.")
self.func(*args, **kwargs)
print("Something is happening after the function is called.")
@MyDecorator
def say_hello():
print("Hello!")
say_hello()
Useful Decorators
@property
- Purpose: Converts a method to a property, allowing it to be accessed like an attribute. This is useful for controlled attribute access and for executing additional logic on attribute get/set.
- Usage: Applied to a method in a class to make it a property.
class MyClass:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
self._value = new_value
@classmethod
- Purpose: Defines a method that operates on the class rather than an instance. Commonly used for alternative constructors.
- Usage: The method takes
clsas the first parameter instead ofself.
class MyClass:
@classmethod
def alternative_constructor(cls):
return cls('default value')
@staticmethod
- Purpose: Defines a method that does not access or modify class or instance state.
- Usage: Can be called on an instance or the class itself.
class MyClass:
@staticmethod
def utility_function():
return 'utility value'
@lru_cache
- Purpose: Caches the results of function calls based on their input arguments to improve performance.
- Usage: Particularly beneficial for expensive or I/O bound function calls.
from functools import lru_cache
@lru_cache(maxsize=100)
def expensive_function(arg):
# Expensive computation or I/O here
return result
@retry
- Purpose: Automatically retries a function call if it raises an exception, until a specified condition is met.
- Usage: Common in scenarios with potential for intermittent failures, like network requests.
from retrying import retry
@retry(stop_max_attempt_number=3, wait_fixed=2000)
def unreliable_function():
# Function that might fail
pass
@contextmanager
- Purpose: Enables the creation of objects that can be used with the
withstatement, for resource management. - Usage: Useful for ensuring resources are properly cleaned up.
from contextlib import contextmanager
@contextmanager
def resource_manager():
resource = acquire_resource()
try:
yield resource
finally:
release_resource(resource)
@wraps
- Purpose: Used within decorator definitions to preserve the wrapped function's metadata.
- Usage: Ensures that the original function's information like name and docstring is retained.
from functools import wraps
def my_decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
# Pre-function logic
result = f(*args, **kwargs)
# Post-function logic
return result
return wrapper
@singledispatch
- Purpose: Enables function overloading based on the type of the first argument, supporting polymorphism.
- Usage: Decorate a generic function, then use the
.register()method to add implementations for specific types.
from functools import singledispatch
@singledispatch
def func(arg):
return "default"
@func.register(int)
def _(arg):
return "integer"
@func.register(str)
def _(arg):
return "string"