Python Decorators — Quick introduction

Trisit Chatterjee
4 min readJul 11, 2024

--

Decorators are one of the most powerful tools that Python supports. They allow you to modify the behaviour of a function or a class. Typically, decorators help extend functionality through existing code in an immaculate and maintainable way. By understanding decorators, you can enhance your code’s flexibility and reusability. This article will delve into what decorators are, how they work, and provide illustrative examples to demonstrate their usage effectively.

The Decorator is an example of metaprogramming, where one writes code preoccupied with other code. In Python, decorators are usually created as functions that take another function as an argument and extend or provide extra functionality. This is to be done without alteration or modification to the source code of the original function. So in that way, it gives a mechanism to attach the desired feature in a reusable manner to functions.

The simplest form of a Python decorator would be to define inside the definition of the decorator function a wrapper function. The wrapper function is the place where you insert the extended behavior. Here is a basic example of a decorator that prints a message before and after the function call:

def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()

In this program, my_decorator is a decorator for say_hello. When you call say_hello, the function within my_decorator is called, prints the message at first, then calls the original function say_hello, and finally prints a message afterwards.

You can also have decorators that accept arguments. Here’s an example of a decorator that repeats a function call a specified number of times:

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("Alice")

The output will be:

Hello, Alice!

Hello, Alice!

Hello, Alice!

Another very common use of decorators is logging. You can create a decorator that logs the arguments and returns the value of a function:

import functools

def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args {args} and kwargs {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper

@log
def add(a, b):
return a + b

print(add(3, 5))

The output will be like below:

Calling add with args (3, 5) and kwargs {}

add returned 8

8

In this example, the log decorator uses the functools.wraps decorator to preserve the original function's metadata. A wrapper function that logs the arguments and return value of the add function. Now, if you call add(3, 5), it's going to log all the calls with their respective results.

Additional Examples and Use Cases

Timing Functions

Another practical use case for decorators is timing how long a function takes to execute. This can be very useful for performance testing.

import time

def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper

@timer
def slow_function():
time.sleep(2)
print("Function complete")

slow_function()

In this example, the timer decorator measures the time before and after the function execution and prints the duration.

Access Control

Decorators can also be used for access control in your code. For instance, you can create a decorator to check if a user has the required permissions to execute a function.

def requires_permission(permission):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
user_permissions = get_user_permissions() # This would be a function to get the current user's permissions
if permission not in user_permissions:
raise PermissionError(f"User lacks {permission} permission")
return func(*args, **kwargs)
return wrapper
return decorator

@requires_permission('admin')
def delete_user(user_id):
print(f"User {user_id} deleted")

# Assuming get_user_permissions returns ['admin']
delete_user(42)

Here, the requires_permission decorator ensures that the function it decorates can only be called if the current user has the specified permission.

PS:

Just in case you are curious how @functools.wraps works?

@functools.wraps is itself a decorator that takes a function used in a wrapper and updates the wrapper function to look more like the wrapped function by copying attributes such as the name, module, and docstring.

Here is what @functools.wraps does in detail:

  • It copies the original function’s __name__, __module__, __annotations__, and __doc__ attributes to the wrapper function.
  • It updates the wrapper function’s __dict__ with the original function’s __dict__.

Conclusion

In summary, Python decorators are a flexible tool to modify or extend the behaviour of functions and methods, which allows for writing cleaner and more maintainable code. The above examples demonstrate how you can use decorators to do things like print messages, reapply function calls, logging, measure performance, and enforce access control. By working through these examples, you will learn how to use decorators to make your code readable and succinct.

--

--

Trisit Chatterjee

Loves Mainframe technologies, Big Data Technologies, Philosophy and Psychology