Python decorators are a powerful feature that allows you to modify or enhance the functionality of functions or classes without changing their source code. Decorators are essentially functions that take another function or class as an argument, perform some operation on it, and return a new function or class. In this article, we will explore Python decorators in-depth, including how to define and use them, common use cases, and examples.
Defining Decorators
To define a decorator in Python, you need to define a function that takes a function as an argument, performs some operation on it, and returns the modified function. Here is an example of a decorator that adds a timer to a function:
import time
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Elapsed time: {end_time - start_time} seconds")
return result
return wrapper
In this example, the timer
function takes a function as an argument (func
) and defines a new function (wrapper
) that wraps the original function. The wrapper
function takes arbitrary arguments (*args
and **kwargs
) and uses the time
module to record the start and end time of the function execution. After the function is executed, the elapsed time is printed to the console, and the result is returned.
Using Decorators
To use a decorator, you simply need to apply it to a function or class using the @
syntax. Here is an example of applying the timer
decorator to a function:
@timer
def my_function():
# Do some work here
In this example, the timer
decorator is applied to the my_function
function using the @
syntax. This means that when my_function
is called, it will first be passed to the timer
function, which will wrap it in the wrapper
function. The resulting function will then be executed, and the elapsed time will be printed to the console.
Note that decorators can also be applied to class definitions, as well as methods within a class. In these cases, the decorator is applied to the entire class or method, rather than just a single function.
Common Use Cases
There are many use cases for Python decorators, but some of the most common ones include:
-
Timing functions: Decorators can be used to add timing information to functions, as shown in the example above.
-
Logging functions: Decorators can be used to log information about function calls, such as the arguments passed and the return value.
-
Validating function arguments: Decorators can be used to validate function arguments before the function is executed.
-
Caching function results: Decorators can be used to cache function results so that expensive computations are only performed once.
-
Enforcing security: Decorators can be used to enforce security policies, such as checking user permissions before allowing access to a function.
Examples
Here are some examples of Python decorators in action:
Timing a function:
import time
def timer(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Elapsed time: {end_time - start_time} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(5)
print("Function executed")
slow_function()
In this example, the timer
decorator is applied to the slow_function
function, which takes 5 seconds to execute. When the function is called, the elapsed time is printed to the console, showing that it took approximately 5 seconds to execute.
Logging function calls:
def logger(func):
def wrapper(*args, **kwargs):
print(f"Function {func.__name__} called with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} returned {result}")
return result
return wrapper
@logger
def add_numbers(a, b):
return a + b
add_numbers(2, 3)
In this example, the logger
decorator is applied to the add_numbers
function, which simply adds two numbers together. When the function is called, the decorator prints information about the function call to the console, including the function name, arguments, and return value.
Validating function arguments:
def validate_args(func):
def wrapper(*args, **kwargs):
for arg in args:
if not isinstance(arg, int):
raise TypeError("Arguments must be integers")
result = func(*args, **kwargs)
return result
return wrapper
@validate_args
def multiply_numbers(a, b):
return a * b
multiply_numbers(2, "3")
In this example, the cache
decorator is applied to the fibonacci
function, which computes the nth Fibonacci number recursively. The decorator maintains a dictionary of previously computed results so that the function only needs to be called once for each value of n
.
Enforcing security:
def check_permissions(func):
def wrapper(user, *args, **kwargs):
if user == "admin":
result = func(*args, **kwargs)
return result
else:
raise ValueError("User does not have permission to access this function")
return wrapper
@check_permissions
def delete_database():
# Delete the database here
pass
delete_database("admin")
In this example, the check_permissions
decorator is applied to the delete_database
function, which deletes the database. The decorator checks whether the user calling the function is an admin, and raises an error if they are not.
Creating class decorators:
Python decorators can also be applied to classes, allowing us to modify the behaviour of an entire class. Here's an example of a class decorator that adds a method to the class:
def add_method(cls):
def new_method(self, x):
return x + 1
cls.new_method = new_method
return cls
@add_method
class MyClass:
pass
obj = MyClass()
print(obj.new_method(1)) # prints 2
In this example, the add_method
decorator is applied to the MyClass
class, which initially has no methods. The decorator adds a new method to the class called new_method
, which simply adds 1 to its argument. When an instance of the class is created and new_method
is called, it correctly returns 2.
Conclusion:
Python decorators are a powerful and flexible tool for modifying the behaviour of functions and classes. They allow you to add functionality like logging, validation, caching, and security to your code, without cluttering up the function or class definition itself. Decorators also enable you to reuse code across multiple functions and classes, making your code more modular and easier to maintain. With this deep dive into Python decorators, you should now have a solid understanding of how they work and how you can use them to make your code more efficient and powerful.