DEV Community

Tarun Sharma
Tarun Sharma

Posted on • Originally published at tapstechie.hashnode.dev

1

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)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more