How to Apply Decorators to Methods Correctly in Python
Decorators are a cornerstone of advanced Python programming, allowing you to modify or enhance the behavior of methods dynamically. However, applying decorators to class methods involves nuances that don't exist with standalone functions, such as handling the self argument and preserving metadata.
This guide explains how to write robust method decorators, handle instance context, and stack multiple decorators effectively.
Understanding Method Decorators
A method decorator is simply a function that takes a method as input and returns a new function (wrapper) that typically calls the original method.
Unlike standalone functions, methods are bound to an object instance (self). Your wrapper function must accept self (and other arguments) to pass them correctly to the original method.
Handling self and Arguments
The most common pitfall is forgetting to account for self in the wrapper function's signature. The universal pattern uses *args and **kwargs, where self is implicitly captured in args[0] or explicitly defined.
Pattern 1: Explicit self (Cleaner for Methods)
If you know the decorator is only for methods, defining self explicitly makes the code more readable.
def log_method_call(func):
def wrapper(self, *args, **kwargs):
print(f"Calling method '{func.__name__}' on instance {self}")
return func(self, *args, **kwargs)
return wrapper
class Calculator:
@log_method_call
def add(self, x, y):
return x + y
calc = Calculator()
calc.add(5, 3)
Output:
Calling method 'add' on instance <__main__.Calculator object at ...>
Pattern 2: Universal Decorator (Works on Functions too)
If you want the decorator to work on both standalone functions and class methods, treat self as just another argument in *args.
def timer(func):
def wrapper(*args, **kwargs):
print("Starting timer...")
result = func(*args, **kwargs)
print("Timer stopped.")
return result
return wrapper
class Task:
@timer
def run(self):
print("Running task...")
t = Task()
t.run()
Output:
Starting timer...
Running task...
Timer stopped.
Preserving Metadata with functools.wraps
When you wrap a function, the new function (wrapper) loses the original function's name (__name__) and docstring (__doc__). This can break introspection tools and make debugging harder.
Always use @functools.wraps(func) on your wrapper.
import functools
def validate_positive(func):
@functools.wraps(func) # ✅ Correct: Preserves metadata
def wrapper(self, x, y):
if x < 0 or y < 0:
raise ValueError("Inputs must be positive")
return func(self, x, y)
return wrapper
class MathOps:
@validate_positive
def multiply(self, x, y):
"""Multiplies two positive numbers."""
return x * y
# Verify metadata
m = MathOps()
print(f"Method Name: {m.multiply.__name__}")
print(f"Docstring: {m.multiply.__doc__}")
Output:
Method Name: multiply
Docstring: Multiplies two positive numbers.
Decorator Stacking Order
You can apply multiple decorators to a single method. The order matters: decorators are applied from bottom to top (innermost to outermost).
Logic: @Dec1(@Dec2(Method)) -> Dec1 wraps Dec2, which wraps Method.
def bold(func):
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
class TextFormatter:
@bold # Executed LAST (Outer layer)
@italic # Executed FIRST (Inner layer)
def format(self, text):
return text
tf = TextFormatter()
print(tf.format("Hello"))
Output:
<b><i>Hello</i></b>
If you swap the order (@italic then @bold), the output becomes <i><b>Hello</b></i>.
Conclusion
To apply decorators to methods correctly:
- Accept Arguments: Ensure your wrapper accepts
*argsand**kwargsto handleselfand method parameters seamlessly. - Preserve Metadata: Always use
@functools.wraps(func)to keep the original method's name and docstring. - Return Values: Don't forget to
return func(...)inside the wrapper, or your method will returnNone. - Mind the Order: Remember that decorators stack from the bottom up.