What is a Decorator?
Imagine you have a gift box; a decorator is like the wrapping paper that adds extra functionality and embellishments to the gift (the original function) without changing it. In Python, a decorator is an advanced syntax that allows you to add additional functionality to a function without modifying its original code.
A decorator is essentially a function that takes another function as an argument and returns a new function. Python uses the<span>@</span> symbol to succinctly apply decorators.
Why Use Decorators?
The main purposes of decorators include:
- Code Reusability: Avoid repeating the same auxiliary code in multiple functions.
- Separation of Concerns: Separate core logic from auxiliary functionalities (like logging, timing, etc.).
- Dynamic Functionality Addition: Add new features without changing the original function’s code.
- Readability: Make the code structure clearer and intentions more explicit.
Decorator Basics: Starting with Functions
To understand decorators, you first need to grasp several characteristics of functions in Python:
- Functions can be passed as arguments.
- Functions can return another function.
- Functions can be defined within other functions.
Let’s look at a simple example of a decorator:
def simple_decorator(func): def wrapper(): print("Before function execution...") func() print("After function execution...") return wrapper@simple_decoratordef say_hello(): print("Hello!")say_hello()
Output:
Before function execution...Hello!After function execution...
Decorators for Functions with Parameters
In the example above, the<span>say_hello</span> function has no parameters. How do we handle decorators for functions that take parameters?
def decorator_with_args(func): def wrapper(*args, **kwargs): print(f"Preparing to execute {func.__name__}, parameters: {args}, {kwargs}") result = func(*args, **kwargs) print(f"{func.__name__} execution completed") return result return wrapper@decorator_with_argsdef add(a, b): return a + bprint(add(2, 3))
Output:
Preparing to execute add, parameters: (2, 3), {}add execution completed5
Decorators with Parameters
Sometimes, we also need the decorator itself to accept parameters. This requires an additional layer of function nesting:
def repeat(num_times): def decorator_repeat(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("Python")
Output:
Hello PythonHello PythonHello Python
Practical Application Scenarios
1. Performance Testing (Timer)
import time
def timer(func): def wrapper(*args, **kwargs): start_time = time.perf_counter() result = func(*args, **kwargs) end_time = time.perf_counter() print(f"{func.__name__} execution time: {end_time - start_time:.4f} seconds") return result return wrapper@timerdef long_running_func(n): return sum(i * i for i in range(n))long_running_func(1000000)
2. Logging
def log(func): def wrapper(*args, **kwargs): print(f"Calling: {func.__name__}, parameters: {args}, {kwargs}") result = func(*args, **kwargs) print(f"{func.__name__} returned: {result}") return result return wrapper@logdef multiply(a, b): return a * bmultiply(3, 4)
3. Permission Validation
def requires_login(func): def wrapper(user, *args, **kwargs): if user.is_authenticated: return func(user, *args, **kwargs) else: raise PermissionError("User not logged in") return wrapperclass User: def __init__(self, name, is_authenticated): self.name = name self.is_authenticated = is_authenticated@requires_logindef view_profile(user): print(f"Viewing {user.name}'s profile")user1 = User("Zhang San", True)user2 = User("Li Si", False)view_profile(user1) # Normal executionview_profile(user2) # Raises PermissionError
Class Decorators
Decorators can also be applied to classes:
def add_method(cls): def decorator(func): setattr(cls, func.__name__, func) return func return decorator@add_method(str)def shout(self): return self.upper() + "!!!"print("hello".shout()) # Output: HELLO!!!
Built-in Decorators
Python comes with several useful decorators:
<span>@property</span>– Turns a method into a property.<span>@classmethod</span>– Defines a class method.<span>@staticmethod</span>– Defines a static method.
class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): if value > 0: self._radius = value else: raise ValueError("Radius must be positive") @property def area(self): return 3.14 * self._radius ** 2 @classmethod def from_diameter(cls, diameter): return cls(diameter / 2) @staticmethod def is_valid_radius(radius): return radius > 0circle = Circle.from_diameter(10)print(circle.radius) # 5.0print(circle.area) # 78.5print(Circle.is_valid_radius(-1)) # False
Decorator Stacking
A function can have multiple decorators applied to it, and their execution order is from the innermost to the outermost (the closest to the function executes first):
def decorator1(func): def wrapper(): print("Decorator 1 before") func() print("Decorator 1 after") return wrapperdef decorator2(func): def wrapper(): print("Decorator 2 before") func() print("Decorator 2 after") return wrapper@decorator1@decorator2def hello(): print("Hello!")hello()
Output:
Decorator 1 beforeDecorator 2 beforeHello!Decorator 2 afterDecorator 1 after
Considerations
- Function Metadata: Decorators can overwrite the original function’s
<span>__name__</span>,<span>__doc__</span>, and other metadata. You can use<span>functools.wraps</span>to preserve this information:
from functools import wrapsdef preserve_metadata(func): @wraps(func) def wrapper(*args, **kwargs): """Wrapper function's docstring""" return func(*args, **kwargs) return wrapper
-
Debugging: When debugging decorated functions, the stack trace will show the decorator’s wrapper function, which can complicate debugging.
-
Performance: Decorators add a slight performance overhead, but in most cases, it can be ignored.
Conclusion
Decorators are a powerful and elegant feature in Python, providing a clear way to modify or extend the behavior of functions and classes. Through decorators, we can:
- Keep code DRY (Don’t Repeat Yourself).
- Separate cross-cutting concerns.
- Improve code readability.
- Achieve flexible functionality extension.
Just like dressing functions in different “clothes”, decorators allow us to easily add various “decorations” to the code without changing its core functionality. Mastering decorators gives you an important tool to make your Python code more elegant and powerful.