Python Decorators allow you to add specific processing before or after the execution of a function without changing the function’s code directly. They are very useful when implementing processing that you want to apply to multiple functions (cross-cutting concerns), such as outputting logs, measuring execution time, or checking access permissions.
This article explains the mechanism of decorators, how to implement them using the @ syntax, and how to write practical decorators that handle arguments and return values.
Basic Concept of Decorators
A decorator is essentially a “higher-order function that takes a function as an argument and returns a new function (the original function with added processing).” First, let’s look at how a decorator works in code without using the @ syntax.
# 1. Decorator Function
def simple_logger(func):
"""
Function that adds log output before and after the received function.
"""
# 2. Define a new function (wrapper function) inside
def wrapper():
print("--- Process Start ---")
func() # Execute the original function
print("--- Process End ---")
# 3. Return the new function
return wrapper
# Function to be decorated
def greet():
print("Hello!")
# --- Applying the Decorator ---
# Pass the greet function to simple_logger and receive the new function
decorated_greet = simple_logger(greet)
# Execute the new function
decorated_greet()
Output:
--- Process Start ---
Hello!
--- Process End ---
We added “Process Start” and “Process End” logs without rewriting the greet function code at all. This is the basic mechanism of a decorator.
Writing with the @ Syntax
In Python, by writing @DecoratorName immediately before the function definition, you can automatically perform the “function replacement” described above. This is called “Syntactic Sugar.”
# Decorator definition (same as before)
def simple_logger(func):
def wrapper():
print("--- Process Start ---")
func()
print("--- Process End ---")
return wrapper
# Apply decorator using @ syntax
@simple_logger
def say_bye():
print("Goodbye!")
# Just calling it executes the decorated function
say_bye()
Output:
--- Process Start ---
Goodbye!
--- Process End ---
Writing @simple_logger is equivalent to executing say_bye = simple_logger(say_bye).
General-Purpose Decorators Handling Arguments and Return Values
The wrapper function in the previous example did not accept arguments, so it would cause an error if applied to a function that requires arguments. Also, if the original function returned a value, it would be lost because the wrapper didn’t return it. To create a general-purpose decorator that works with any function, we use variable-length arguments (*args, **kwargs) and return.
Practical Code Example: Logging Execution Results
Here is a decorator that displays the function’s arguments and its execution result (return value).
def result_logger(func):
"""
Decorator to log function arguments and return value.
"""
# Accept any arguments with *args, **kwargs
def wrapper(*args, **kwargs):
print(f"[Log] Function '{func.__name__}' called.")
# Execute the original function and store the return value
result = func(*args, **kwargs)
print(f"[Log] Return Value: {result}")
# Return the result to the caller
return result
return wrapper
# --- Using the Decorator ---
@result_logger
def add_numbers(a, b):
return a + b
@result_logger
def multiply(x, y):
return x * y
# Execution
sum_val = add_numbers(10, 20)
print(f"Sum: {sum_val}\n")
product_val = multiply(5, 4)
print(f"Product: {product_val}")
Output:
[Log] Function 'add_numbers' called.
[Log] Return Value: 30
Sum: 30
[Log] Function 'multiply' called.
[Log] Return Value: 20
Product: 20
Explanation
def wrapper(*args, **kwargs):: Defined to accept any arguments.result = func(*args, **kwargs): Passes the received arguments exactly as they are to the original function.return result: Returns the result of the original function as the return value of the wrapper function. If you forget this, the decorated function will always returnNone.
Summary
- A decorator is a mechanism to wrap a function and add functionality.
- You call the original function inside
def wrapper()and return thatwrapper. - Using
@DecoratorNameallows for concise application. - To support functions with arguments and return values, the standard practice is to accept
*args,**kwargsin the wrapper function and usereturnat the end.
