Decorators in Python provide a way to modify or extend the behavior of a function without changing its actual code. They are applied using the @decorator_name syntax above a function definition. Decorators are widely used for logging, access control, performance measurement, authentication, and many other tasks.
A decorator wraps another function and allows execution of additional code before or after the wrapped function runs. This helps in writing reusable functionality that can be applied to multiple functions.
In Python, functions are treated as objects. This means they can be stored in variables, passed as arguments, or returned from other functions. Decorators rely on this property.
def greet():
return "Hello"x = greet
print(x())
Output:
Hello
The function greet is assigned to x, demonstrating that functions can be passed around like data.
A decorator is a function that takes another function as input and returns a new function.
def my_decorator(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapperdef say_hello():
print("Hello")decorated = my_decorator(say_hello)
decorated()
Output:
Before function call
Hello
After function call
Here, the original function say_hello() is wrapped with additional behavior.
Python provides a cleaner and more readable way to apply decorators using the @ symbol.
def my_decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper@my_decorator
def show():
print("Inside function")show()
Output:
Before function
Inside function
After function
The decorator is applied directly to the show function.
If a function accepts arguments, the wrapper must also accept them and pass them forward.
def log(func):
def wrapper(a, b):
print(f"Calling function with {a} and {b}")
return func(a, b)
return wrapper@log
def add(x, y):
return x + yprint(add(5, 3))
Output:
Calling function with 5 and 3
8
Decorators should return the result of the wrapped function when needed.
def square_decorator(func):
def wrapper(n):
result = func(n)
return result * result
return wrapper@square_decorator
def get_number(n):
return nprint(get_number(4))
Output:
16
To support any number of arguments, decorators should use *args and **kwargs.
def logger(func):
def wrapper(*args, **kwargs):
print("Arguments:", args, kwargs)
return func(*args, **kwargs)
return wrapper@logger
def multiply(a, b, c=1):
return a * b * cprint(multiply(2, 3, c=4))
Output:
Arguments: (2, 3) {'c': 4}
24
Sometimes a decorator needs its own arguments. In that case, we create a decorator function that returns another decorator.
def repeat(times):
def decorator(func):
def wrapper():
for _ in range(times):
func()
return wrapper
return decorator@repeat(3)
def greet():
print("Hello")greet()
Output:
Hello
Hello
Hello
Without wraps(), a decorated function loses its original name and docstring. This can be fixed using functools.wraps.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper():
print("Wrapper executed")
return func()
return wrapper@my_decorator
def hello():
print("Hello")print(hello.__name__)
Output:
Wrapper executed
Hello
hello
hello.__name__ stays as hello, not wrapper.