Posted on Aug 16, 2025· Updated on Aug 16, 2025

Декораторы Python: Простые шаблоны для улучшения кода – Шпаргалка

#python #intermediate #decorators
Image for Декораторы Python: Простые шаблоны для улучшения кода – Шпаргалка

Вы знаете это чувство, когда видите @something над функцией и гадаете, какая черная магия там происходит? Я тоже через это проходил. Декораторы могут показаться устрашающими, но на самом деле это одна из самых элегантных особенностей Python, как только вы поймете основы — см. Декораторы (шпаргалка) для краткого справочника.

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

Самый простой декоратор

Давайте начнем с самого базового примера, чтобы понять, что происходит:

def my_decorator(func):
    def wrapper():
        print("Something happens before!")
        func()
        print("Something happens after!")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# Something happens before!
# Hello!
# Something happens after!

Вот и все! Декоратор — это просто функция, которая принимает другую функцию и оборачивает ее дополнительным поведением. Синтаксис @my_decorator — это просто более чистый способ написать say_hello = my_decorator(say_hello).

Ваш первый полезный декоратор: Таймер

Вот декоратор, который вы действительно захотите использовать — тот, который сообщает вам, сколько времени занимает выполнение ваших функций:

import time
import functools

def timer(func):
    @functools.wraps(func)  # Сохраняет имя и документацию исходной функции
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done!"

result = slow_function()
# slow_function took 1.0041 seconds
print(result)  # Done!

См. Декораторы (шпаргалка) для дополнительных шаблонов декораторов и общих паттернов.

Видите, как мы используем *args и **kwargs (см. Аргументы и ключевые аргументы)? Это позволяет нашему декоратору работать с любой функцией, независимо от того, сколько аргументов она принимает.

Отладка вашего кода: Декоратор-логгер

Когда вы пытаетесь выяснить, что идет не так, этот декоратор невероятно полезен — также проверьте Отладка (шпаргалка) для сопутствующих советов и методов:

def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_str = ', '.join(repr(arg) for arg in args)
        kwargs_str = ', '.join(f"{k}={v!r}" for k, v in kwargs.items())
        all_args = ', '.join(filter(None, [args_str, kwargs_str]))
        print(f"Calling {func.__name__}({all_args})")

        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper

@debug
def add_numbers(a, b, multiply_by=1):
    return (a + b) * multiply_by

result = add_numbers(5, 3, multiply_by=2)
# Calling add_numbers(5, 3, multiply_by=2)
# add_numbers returned 16

Управление доступом: Декоратор аутентификации

Хотите убедиться, что только определенные пользователи могут запускать функцию? Вот как это сделать:

def requires_auth(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # В реальном приложении вы бы проверяли фактическую аутентификацию
        user_logged_in = True  # Это пришло бы из вашей системы аутентификации

        if not user_logged_in:
            return "Access denied! Please log in."

        return func(*args, **kwargs)
    return wrapper

@requires_auth
def delete_everything():
    return "💥 Everything deleted! (just kidding)"

result = delete_everything()
print(result)  # 💥 Everything deleted! (just kidding)

Ускорение работы: Декоратор кэширования

Если у вас есть функция, которая выполняет дорогостоящие вычисления с одинаковыми входными данными, закэшируйте результаты:

def cache(func):
    cached_results = {}

    @functools.wraps(func)
    def wrapper(*args):
        if args in cached_results:
            print(f"Cache hit for {func.__name__}{args}")
            return cached_results[args]

        print(f"Computing {func.__name__}{args}")
        result = func(*args)
        cached_results[args] = result
        return result

    return wrapper

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))
# Computing fibonacci(10)
# Computing fibonacci(9)
# Computing fibonacci(8)
# ... (lots of computation)
# Cache hit for fibonacci(2)
# Cache hit for fibonacci(3)
# ... (cache hits)
# 55

Повтор неудачных операций

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

import random

def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Attempt {attempt + 1} failed: {e}")
                    if attempt == max_attempts - 1:
                        print("All attempts failed!")
                        raise
        return wrapper
    return decorator

@retry(max_attempts=3)
def unreliable_api_call():
    if random.random() < 0.7:  # 70% chance of failure
        raise Exception("Network error")
    return "Success!"

# Это повторит попытку до 3 раз, если произойдет сбой
result = unreliable_api_call()

Ограничение скорости: Замедлите ваш код

Иногда вам нужно быть осторожными с API или базами данных:

import time
import functools

def rate_limit(seconds):
    """
    A decorator to limit how frequently a function can be called.
    """
    def decorator(func):
        # Используем список для хранения изменяемого значения float последнего времени вызова.
        # Это позволяет внутренней функции-обертке его изменять.
        last_called_at = [0.0]

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Вычисляем время, прошедшее с момента последнего вызова
            elapsed = time.time() - last_called_at[0]
            wait_time = seconds - elapsed

            # Если прошло недостаточно времени, ждем остаток
            if wait_time > 0:
                time.sleep(wait_time)

            # Обновляем время последнего вызова и выполняем функцию
            last_called_at[0] = time.time()
            return func(*args, **kwargs)

        return wrapper
    return decorator

@rate_limit(1)  # Разрешить не более одного вызова в секунду
def call_api():
    print(f"API called at {time.time():.2f}")

# Эти вызовы будут разделены примерно на 1 секунду каждый
call_api()
call_api()
call_api()

# Ожидаемый вывод:
# API called at 1723823038.50
# API called at 1723823039.50
# API called at 1723823040.50

Проверка ваших входных данных

Убедитесь, что ваши функции получают данные правильного типа:

def validate_types(**expected_types):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Получаем имена параметров функции
            import inspect
            sig = inspect.signature(func)
            bound_args = sig.bind(*args, **kwargs)
            bound_args.apply_defaults()

            for param_name, expected_type in expected_types.items():
                if param_name in bound_args.arguments:
                    value = bound_args.arguments[param_name]
                    if not isinstance(value, expected_type):
                        raise TypeError(
                            f"{param_name} must be {expected_type.__name__}, "
                            f"got {type(value).__name__}"
                        )

            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(name=str, age=int)
def create_user(name, age):
    return f"User {name}, age {age}"

# Это работает
user1 = create_user("Alice", 25)
print(user1)  # User Alice, age 25

# Это вызывает TypeError
try:
    user2 = create_user("Bob", "twenty-five")
except TypeError as e:
    print(e)  # age must be int, got str

Когда использовать каждый декоратор

Тип декоратораЛучше всего подходит дляПримеры использования
ТаймерМониторинг производительностиПоиск медленных функций, оптимизация
Отладчик/ЛоггерРазработка и устранение неполадокПонимание вызовов функций, отладка
АутентификацияБезопасность и контроль доступаЗащита административных функций, права пользователей
КэшДорогостоящие вычисленияЗапросы к базе данных, вызовы API, сложные вычисления
ПовторНенадежные операцииСетевые запросы, файловые операции
Ограничение скоростиКонтроль частотыВызовы API, предотвращение спама
ВалидацияЦелостность данныхВвод пользователя, параметры API

Советы по использованию декораторов

Всегда используйте @functools.wraps — это сохраняет имя и документацию исходной функции, облегчая отладку (см. Шпаргалка по декораторам для примеров).

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

Подумайте о порядке — при наложении декораторов тот, что ближе всего к функции, выполняется первым:

@timer
@debug
def my_function():
    pass

# Это то же самое, что:
# my_function = timer(debug(my_function))

Не злоупотребляйте ими — декораторы мощные, но их слишком много может затруднить чтение кода.

Ключевые выводы

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

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

Хотите попрактиковаться? Попробуйте добавить декоратор @timer к некоторым из ваших существующих функций и посмотрите, какие из них работают медленнее, чем вы ожидали. Вы можете удивиться тому, что обнаружите!

Связанные ссылки

Добавьте соответствующие внутренние ссылки на документацию ниже для дальнейшего чтения: