DEV Community

francesco agati
francesco agati

Posted on

Python Decorators: Simplified Explanation

Python decorators are a powerful feature that allows you to modify or extend the behavior of functions or methods without changing their actual code. Let’s explore how decorators work with some simple examples.

Example 1: Logging Decorator

The logging decorator adds functionality to log information about when a function is called and what it returns.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f'Calling {func.__name__} with args={args}, kwargs={kwargs}')
        result = func(*args, **kwargs)
        print(f'{func.__name__} returned {result}')
        return result
    return wrapper
Enter fullscreen mode Exit fullscreen mode

When you decorate a function with @logger, it prints messages before and after calling the function, showing its arguments and return value.

Example 2: Timing Decorator

The timing decorator measures how much time a function takes to execute.

import time

def timer(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} seconds to execute')
        return result
    return wrapper
Enter fullscreen mode Exit fullscreen mode

With @timer applied to a function, it calculates and prints the execution time in seconds.

Example 3: Authentication Decorator

The authentication decorator restricts access to a function based on user login status.

logged_in_users = ['alice', 'bob']

def authenticate(func):
    def wrapper(username, *args, **kwargs):
        if username in logged_in_users:
            return func(username, *args, **kwargs)
        else:
            raise PermissionError(f'User {username} is not logged in')
    return wrapper
Enter fullscreen mode Exit fullscreen mode

When decorated with @authenticate, the function can only be accessed by users in logged_in_users.

Example 4: Memoization Decorator

The memoization decorator caches results of a function to optimize performance for repeated calls with the same arguments.

def memoize(func):
    cache = {}

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

    return wrapper
Enter fullscreen mode Exit fullscreen mode

Functions decorated with @memoize store computed results, returning cached results for identical arguments to avoid redundant calculations.

Using Decorators

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

@timer
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

@authenticate
def protected_function(username, message):
    return f'{username}: {message}'

@memoize
def factorial(n):
    if n == 0 or n == 1:
        return 1
    else:
        return n * factorial(n-1)

# Testing each decorated function

print(add(3, 5))  # Output: Calling add with args=(3, 5), kwargs={}, add returned 8, 8

print(fibonacci(10))  # Output: fibonacci took 0.0 seconds to execute, 55

print(protected_function('alice', 'Hello!'))  # Output: alice: Hello!

try:
    print(protected_function('eve', 'Hi!'))  # Raises PermissionError
except PermissionError as e:
    print(e)

print(factorial(5))  # Output: 120

print(factorial(3))  # Output: 6
Enter fullscreen mode Exit fullscreen mode

Each decorator adds specific functionality to the decorated functions:

  • @logger logs function calls and returns.
  • @timer measures execution time.
  • @authenticate restricts function access based on user login.
  • @memoize caches function results to enhance performance.

Python decorators are versatile tools for adding cross-cutting concerns to functions, promoting code reuse and enhancing readability. They are widely used in frameworks and libraries to simplify and extend functionality without modifying core code.

Top comments (0)