Python Decorators: Simplified Explanation
Tags: python, decorators
Published: 2024-06-20 20:36:25

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.

```python
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
```

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.

```python
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
```

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.

```python
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
```

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.

```python
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
```

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

#### Using Decorators

```python
@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
```

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.