Skip to main content

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 cls as the first parameter instead of self.
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 with statement, 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"