Introduction
In this article, we will learn about what is Decorators and how we can use them in Python. In Python, decorators are a way to modify the behavior of a function without changing its source code. They provide a simple syntax for calling higher-order functions, which are functions that take other functions as arguments or return them as results. Decorators can be used to add functionality to existing functions, modify their arguments, or even replace them entirely.
What are Decorators?
A decorator is a function that takes another function as an argument, adds some functionality to it, and returns a new function. The new function typically contains the original function's code and the added functionality. To use a decorator, you simply apply it to the function you want to decorate by placing the decorator function's name before the function definition, preceded by the @ symbol.
Syntax
def decorator_function(original_function):
def wrapper_function(*args, **kwargs):
# Code to execute before the original function
result = original_function(*args, **kwargs)
# Code to execute after the original function
return result
return wrapper_function
In the above Syntax, decorator_function takes original_function as an argument and returns a new function wrapper_function. The wrapper_function can execute code before and after calling the original_function, and it can modify the arguments passed to the original function or its return value.
Examples
Logging Decorator
One common use case for decorators is logging. Let's create a decorator that logs the arguments passed to a function and its return value.
def logger(func):
def wrapper(*args, **kwargs):
print(f"Logging: {func.__name__} function was called with arguments {args} and {kwargs}")
return func(*args, **kwargs)
return wrapper
@logger
def add(x, y):
return x + y
result = add(5, 3)
In the above example, the logger decorator prints the function name and arguments every time the decorated function (add) is called.
Caching Decorator
Another common use case for decorators is caching. Let's create a decorator that caches the return value of a function based on its arguments.
def cache(original_function):
cache_data = {}
def wrapper_function(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cache_data:
return cache_data[key]
else:
result = original_function(*args, **kwargs)
cache_data[key] = result
return result
return wrapper_function
@cache
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # Cached after the first call
print(fibonacci(10)) # Returned from cache
In the above example, the cache decorator stores the return value of the Fibonacci function for each set of arguments. If the function is called again with the same arguments, the cached result is returned instead of recalculating the Fibonacci number.
Decorators with Arguments
Decorators can also accept arguments themselves, making them even more flexible. To create a decorator that takes arguments, you define a function that returns the actual decorator.
def decorator_with_args(arg1, arg2):
def decorator_function(original_function):
def wrapper_function(*args, **kwargs):
# Code to execute before the original function
# Can use arg1 and arg2 here
print(f"Decorator arguments: {arg1}, {arg2}")
result = original_function(*args, **kwargs)
# Code to execute after the original function
return result
return wrapper_function
return decorator_function
@decorator_with_args(arg1="value1", arg2="value2")
def original_function(a, b):
result = a + b
return result
print(original_function(3, 5))
In the above example, decorator_with_args takes arg1 and arg2 as arguments and returns the actual decorator function decorator_function. The decorator_function can then access and use arg1 and arg2 within the wrapper_function.
Decorators with Multiple Functions
Decorators can also be applied to multiple functions by using the same decorator function. This can be useful when you want to apply the same functionality to multiple functions without duplicating code.
def logger(original_function):
def wrapper_function(*args, **kwargs):
print(f"Calling {original_function.__name__} with args={args}, kwargs={kwargs}")
result = original_function(*args, **kwargs)
print(f"Function {original_function.__name__} returned {result}")
return result
return wrapper_function
@logger
def add_numbers(a, b):
return a + b
@logger
def multiply_numbers(a, b):
return a * b
result1 = add_numbers(2, 3)
result2 = multiply_numbers(4, 5)
In the above example, both add_numbers and multiply_numbers are decorated with the logger decorator, so their arguments and return values will be logged when called.
Summary
Decorators are a powerful feature in Python that can help you write more modular, reusable, and maintainable code. They provide a way to add functionality to functions without modifying their source code, making it easier to extend existing code and apply cross-cutting concerns like logging, caching, and validation. By understanding how decorators work and how to use them effectively, you can take your Python programming skills to the next level.