Декораторы и модификация поведения функций

Декораторы в Python — это мощный инструмент для модификации поведения функций или методов без изменения их кода. Они позволяют добавлять функциональность к существующим функциям или методам, оборачивая их в другие функции. Декораторы особенно полезны для выполнения задач, таких как логирование, проверка прав доступа, кэширование результатов и другие задачи.

Основы декораторов

Простейший декоратор

Декоратор — это функция, которая принимает другую функцию в качестве аргумента и возвращает новую функцию, которая расширяет или изменяет поведение оригинальной функции.

def my_decorator(func):
    def wrapper():
        print("Что-то делается до вызова функции")
        func()
        print("Что-то делается после вызова функции")
    return wrapper

@my_decorator
def say_hello():
    print("Привет!")

say_hello()

Вывод:

Что-то делается до вызова функции
Привет!
Что-то делается после вызова функции

В этом примере декоратор my_decorator оборачивает функцию say_hello, добавляя дополнительный функционал до и после ее вызова.

Декораторы с аргументами

Если функция, которую вы декорируете, принимает аргументы, декоратор также должен их обрабатывать. Можно использовать *args и **kwargs, чтобы передать произвольное количество аргументов и ключевых слов.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Что-то делается до вызова функции")
        result = func(*args, **kwargs)
        print("Что-то делается после вызова функции")
        return result
    return wrapper

@my_decorator
def greet(name):
    print(f"Привет, {name}!")

greet("Алиса")

Вывод:

Что-то делается до вызова функции
Привет, Алиса!
Что-то делается после вызова функции

Декораторы с возвращаемыми значениями

Декораторы могут возвращать значения, которые вычисляются внутри wrapper функции.

def double_return(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result * 2
    return wrapper

@double_return
def get_number():
    return 5

print(get_number())

Вывод:

10

В этом примере декоратор double_return удваивает результат функции get_number.

Примеры использования декораторов

Логирование

Декораторы могут использоваться для добавления логирования к функциям.

def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов функции {func.__name__} с аргументами {args} и {kwargs}")
        result = func(*args, **kwargs)
        print(f"Функция {func.__name__} вернула {result}")
        return result
    return wrapper

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

add(3, 4)

Вывод:

Вызов функции add с аргументами (3, 4) и {}
Функция add вернула 7

Проверка прав доступа

Декораторы могут проверять права доступа перед выполнением функции.

def requires_admin(func):
    def wrapper(user, *args, **kwargs):
        if not user.get('admin', False):
            raise PermissionError("Доступ запрещен")
        return func(user, *args, **kwargs)
    return wrapper

@requires_admin
def delete_user(user, username):
    print(f"Пользователь {username} удален")

user = {'name': 'Иван', 'admin': True}
delete_user(user, 'Петр')

Вывод:

Пользователь Петр удален

Если user не является администратором, будет вызвано исключение PermissionError.

Кэширование результатов

Декораторы могут использоваться для кэширования результатов функций.

from functools import lru_cache

@lru_cache(maxsize=None)  # Кэширование результатов функции
def compute_expensive_operation(x):
    print(f"Выполнение операции для {x}")
    return x * x

print(compute_expensive_operation(4))
print(compute_expensive_operation(4))

Вывод:

Выполнение операции для 4
16
16

Во втором вызове compute_expensive_operation результат берется из кэша, что ускоряет выполнение.

Вложенные декораторы

Декораторы могут быть вложенными. В этом случае они применяются последовательно, начиная с самой внутренней функции.

def decorator1(func):
    def wrapper(*args, **kwargs):
        print("Декоратор 1")
        return func(*args, **kwargs)
    return wrapper

def decorator2(func):
    def wrapper(*args, **kwargs):
        print("Декоратор 2")
        return func(*args, **kwargs)
    return wrapper

@decorator1
@decorator2
def greet():
    print("Привет!")

greet()

Вывод:

Декоратор 1
Декоратор 2
Привет!

В этом примере decorator2 применяется к функции greet первой, затем decorator1 применяется к результату.

Декораторы с аргументами

Декораторы с аргументами позволяют создавать более гибкие и настраиваемые декораторы, которые могут изменять своё поведение в зависимости от переданных аргументов. Основная идея заключается в создании декоратора, который сам является функцией, принимающей аргументы, и возвращает другой декоратор.

Вот как можно создать и использовать декораторы с аргументами:

Пример 1: Простейший декоратор с аргументами

Создадим декоратор, который принимает параметр и использует его для модификации поведения функции.

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

@repeat(3)
def greet(name):
    print(f"Привет, {name}!")

greet("Алиса")

Вывод:

Привет, Алиса!
Привет, Алиса!
Привет, Алиса!

В этом примере декоратор repeat принимает аргумент n, который указывает, сколько раз нужно выполнить функцию greet.

Пример 2: Логирование с аргументами

Создадим декоратор, который логирует сообщения с разным уровнем важности, передавая уровень как аргумент.

def log(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] Вызов функции {func.__name__} с аргументами {args} и {kwargs}")
            result = func(*args, **kwargs)
            print(f"[{level}] Функция {func.__name__} вернула {result}")
            return result
        return wrapper
    return decorator

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

add(5, 3)

Вывод:

[DEBUG] Вызов функции add с аргументами (5, 3) и {}
[DEBUG] Функция add вернула 8

Декоратор log использует параметр level, чтобы указать уровень важности сообщения.

Создадим декоратор, который проверяет права доступа на основе переданных ролей.

def requires_role(role):
    def decorator(func):
        def wrapper(user, *args, **kwargs):
            if role not in user.get('roles', []):
                raise PermissionError(f"Доступ запрещен для роли {role}")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@requires_role('admin')
def delete_user(user, username):
    print(f"Пользователь {username} удален")

user = {'name': 'Иван', 'roles': ['user', 'admin']}
delete_user(user, 'Петр')

Вывод:

Пользователь Петр удален

Если user не имеет роли 'admin', будет вызвано исключение PermissionError.

Пример 4: Декоратор для кэширования с аргументами

Создадим декоратор для кэширования результатов функции с использованием заданного размера кэша.

from functools import lru_cache

def cache_with_size(maxsize):
    def decorator(func):
        cached_func = lru_cache(maxsize)(func)
        def wrapper(*args, **kwargs):
            return cached_func(*args, **kwargs)
        return wrapper
    return decorator

@cache_with_size(maxsize=3)
def compute_expensive_operation(x):
    print(f"Выполнение операции для {x}")
    return x * x

print(compute_expensive_operation(2))
print(compute_expensive_operation(2))
print(compute_expensive_operation(3))
print(compute_expensive_operation(4))
print(compute_expensive_operation(2))  # Должно взять из кэша

Вывод:

Выполнение операции для 2
4
Выполнение операции для 3
9
Выполнение операции для 4
16
Выполнение операции для 2
4

В этом примере декоратор cache_with_size создает кэш с ограниченным размером для функции compute_expensive_operation.

@wraps

wraps — это декоратор из модуля functools в Python, который используется для правильного оформления декорированных функций. При создании собственных декораторов, когда вы декорируете одну функцию другой, важные метаданные исходной функции, такие как её имя, строка документации (docstring) и аннотации, могут быть потеряны. Декоратор @functools.wraps помогает сохранить эти атрибуты.

Как работает wraps

Когда вы создаёте свой декоратор, вы, вероятно, пишете что-то вроде:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        # код перед вызовом функции
        result = func(*args, **kwargs)
        # код после вызова функции
        return result
    return wrapper

Если вы используете этот декоратор, декорированная функция теряет своё имя и строку документации:

@my_decorator
def example_function():
    """Это пример функции."""
    pass

print(example_function.__name__)  # Вывод: wrapper
print(example_function.__doc__)   # Вывод: None

Чтобы избежать этой проблемы, можно использовать @functools.wraps:

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # код перед вызовом функции
        result = func(*args, **kwargs)
        # код после вызова функции
        return result
    return wrapper

@my_decorator
def example_function():
    """Это пример функции."""
    pass

print(example_function.__name__)  # Вывод: example_function
print(example_function.__doc__)   # Вывод: Это пример функции.

Преимущества использования wraps

  • Сохранение метаданных: имя функции, строка документации и другие атрибуты сохраняются у декорированной функции.

  • Улучшение отладки: при отладке или логировании имя функции и строка документации остаются корректными.

  • Работа с инструментами и библиотеками: многие библиотеки и инструменты (например, тестовые фреймворки) полагаются на атрибуты функций, такие как имя и строка документации.

Использование @functools.wraps — это лучшая практика при создании собственных декораторов в Python. Он гарантирует, что декорированные функции сохраняют свои метаданные, что делает ваш код более читаемым, поддерживаемым и удобным для отладки.

Заключение

Декораторы в Python — это мощный и гибкий инструмент для модификации поведения функций и методов. Они позволяют добавлять функциональность, не изменяя код исходных функций, что делает код более модульным и легко расширяемым. Декораторы могут использоваться для выполнения различных задач, таких как логирование, проверка прав доступа, кэширование и другие.

Декораторы с аргументами предоставляют гибкий способ модификации поведения функций и методов, позволяя настраивать их поведение в зависимости от переданных параметров. Это делает их очень полезными для создания настраиваемых решений, таких как логирование, кэширование и проверка прав доступа.

Last updated