DEV Community

Tarun Sharma
Tarun Sharma

Posted on • Originally published at tapstechie.hashnode.dev

Simplifying Python Decorators: What You Need to Know

1. What is a Decorator?

Definition:

A decorator is a function that takes another function as input and extends or alters its behavior without modifying its actual code. It’s a powerful tool for enhancing code reuse and separating concerns.

Use Case:

Decorators are commonly used for:

  • Logging: Automatically logging information about function calls.

  • Authentication: Checking if a user is authorized to perform an action.

  • Memoization: Caching results of expensive function calls to improve performance.

2. How Decorators Work

Function Decorators: A decorator wraps a function to modify or extend its behavior. The syntax for using a decorator is the @decorator_name syntax placed above the function definition.

Syntax Example:

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()
Enter fullscreen mode Exit fullscreen mode

Output:

Something is happening before the function is called.
Hello!
Something is happening after the function is called.
Enter fullscreen mode Exit fullscreen mode

Explanation:

Here, my_decorator is applied to say_hello. The wrapper function inside the decorator adds behavior before and after the execution of say_hello.

3. Common Use Cases

Logging Decorator Example:

import logging

logging.basicConfig(level=logging.INFO)

def log_decorator(func):
    def wrapper(*args, **kwargs):
        logging.info(f"Function {func.__name__} called with args: {args}, kwargs: {kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"Function {func.__name__} returned {result}")
        return result
    return wrapper

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

add(5, 3)
Enter fullscreen mode Exit fullscreen mode

Output:

INFO:root:Function add called with args: (5, 3), kwargs: {}
INFO:root:Function add returned 8
Enter fullscreen mode Exit fullscreen mode

Explanation:

This logging decorator logs the function name, its arguments, and the result of the function call.

Authorization Decorator Example:

def require_permission(permission):
    def decorator(func):
        def wrapper(user, *args, **kwargs):
            if user.has_permission(permission):
                return func(user, *args, **kwargs)
            else:
                raise PermissionError("User does not have the required permission.")
        return wrapper
    return decorator

class User:
    def __init__(self, name, permissions):
        self.name = name
        self.permissions = permissions

    def has_permission(self, permission):
        return permission in self.permissions

@require_permission('admin')
def delete_user(user):
    print(f"User {user.name} deleted.")

admin_user = User('Admin', ['admin'])
regular_user = User('Regular', [])

delete_user(admin_user)   # User Admin deleted.
delete_user(regular_user) # Raises PermissionError
Enter fullscreen mode Exit fullscreen mode

Explanation:

This authorization decorator checks if a user has the required permission before allowing the function to execute.

Memoization Decorator Example:

def memoize(func):
    cache = {}
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return wrapper

@memoize
def expensive_computation(n):
    print("Computing...")
    return n * n

print(expensive_computation(4))  # Computes and caches
print(expensive_computation(4))  # Uses cache
Enter fullscreen mode Exit fullscreen mode

Output:

Computing...
16
16
Enter fullscreen mode Exit fullscreen mode

Explanation:

This memoization decorator caches the results of expensive computations to avoid redundant calculations.

4. Creating Your Own Decorators

Basic Decorator:

def basic_decorator(func):
    def wrapper(*args, **kwargs):
        print("Doing something before the function")
        result = func(*args, **kwargs)
        print("Doing something after the function")
        return result
    return wrapper

@basic_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")
Enter fullscreen mode Exit fullscreen mode

Output:

Doing something before the function
Hello, Alice!
Doing something after the function
Enter fullscreen mode Exit fullscreen mode

Explanation:

This basic decorator adds behavior before and after the greet function.

Decorator with Arguments:

def repeat(num_times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")
Enter fullscreen mode Exit fullscreen mode

Output:

Hello, Alice!
Hello, Alice!
Hello, Alice!
Enter fullscreen mode Exit fullscreen mode

Explanation:

This decorator allows specifying the number of times a function should be repeated.

Class-Based Decorator:

class ClassDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("Doing something before the function")
        result = self.func(*args, **kwargs)
        print("Doing something after the function")
        return result

@ClassDecorator
def say_hi():
    print("Hi!")

say_hi()
Enter fullscreen mode Exit fullscreen mode

Output:

Doing something before the function
Hi!
Doing something after the function
Enter fullscreen mode Exit fullscreen mode

Explanation:

This class-based decorator provides a more flexible way to define decorators, especially if you need to maintain state.

5. Decorator Chaining

You can apply multiple decorators to a single function by stacking them:

def upper_case_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def exclamation_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result + "!!!"
    return wrapper

@upper_case_decorator
@exclamation_decorator
def greet(name):
    return f"Hello, {name}"

print(greet("Alice"))
Enter fullscreen mode Exit fullscreen mode

Output:

HELLO, ALICE!!!
Enter fullscreen mode Exit fullscreen mode

Explanation:

In this example, the greet function is first decorated with exclamation_decorator and then with upper_case_decorator. The result is a greeting in uppercase followed by exclamation marks.

6. Real-World Use Cases and Best Practices

When to Use Decorators:

  • Code Reusability: Avoid duplicating code by applying common functionalities across multiple functions.

  • Separation of Concerns: Keep your core logic separate from auxiliary functionality like logging or validation.

  • Clean and Readable Code: Decorators help keep code more organized and readable.

Performance Implications:

  • Overhead: Decorators add a layer of function calls which can impact performance. Use them judiciously.

  • Debugging: Be aware that decorators can sometimes obscure function names and arguments, making debugging slightly more complex.


Interview Questions and Answers

  1. What is a decorator in Python?
* A decorator is a function that takes another function and extends or alters its behavior without modifying its code.
Enter fullscreen mode Exit fullscreen mode
  1. What are some common use cases for decorators?
* Logging, authentication, memoization, and validation.
Enter fullscreen mode Exit fullscreen mode
  1. How do you apply multiple decorators to a single function?
* By stacking them above the function definition using the `@decorator_name` syntax.
Enter fullscreen mode Exit fullscreen mode
  1. What is the difference between a function-based decorator and a class-based decorator?
* A function-based decorator is defined using nested functions, while a class-based decorator uses a class with a `__call__` method to maintain state.
Enter fullscreen mode Exit fullscreen mode
  1. Can decorators accept arguments? If so, how?
* Yes, decorators can accept arguments by defining an outer function that takes the arguments and returns the actual decorator function.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)