$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогДекораторы в Python: от синтаксического сахара до функциональных инструментов

Декораторы в Python: от синтаксического сахара до функциональных инструментов

$ sudo teach IT
·18 июля 2026 г.·8 мин·65
Декораторы в Python: от синтаксического сахара до функциональных инструментов

Разбираем декораторы изнутри: как они работают на уровне замыканий, зачем нужен functools.wraps и как писать декораторы с аргументами. Примеры из реального кода — кэширование, логирование, контроль доступа.

Содержание
Функции как объекты первого классаЗамыкания: механизм, на котором всё держитсяПервый декоратор: разбираем по шагамПроблема потери идентичности и functools.wrapsДекораторы с аргументамиДекоратор, работающий и со скобками, и безПрактические паттерныКэширование результатовКонтроль доступаИзмерение времени выполненияСтекирование декораторов и порядок примененияДекораторы классовКогда декораторы — не лучший выборДекораторы и типизацияИтог

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

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

Разберём декораторы по-настоящему: от первых принципов до практических паттернов.


Функции как объекты первого класса

Прежде чем говорить о декораторах, нужно зафиксировать базовый факт: в Python функции — это объекты. Их можно присваивать переменным, передавать как аргументы и возвращать из других функций. Именно на этом фундаменте держится весь механизм декораторов.

code
def greet(name):
    return f"Hello, {name}"

# Функцию можно присвоить переменной
say_hello = greet
print(say_hello("Alice"))  # Hello, Alice

# Функцию можно передать как аргумент
def apply(func, value):
    return func(value)

print(apply(greet, "Bob"))  # Hello, Bob

Это не трюк и не исключение — это фундаментальное свойство языка. Python не делает различий между «обычными» объектами и функциями с точки зрения передачи и хранения.


Замыкания: механизм, на котором всё держится

Декоратор — это, по сути, функция, которая принимает другую функцию и возвращает новую. Чтобы эта новая функция «помнила» оригинальную, используется замыкание.

Замыкание возникает, когда вложенная функция ссылается на переменную из охватывающей области видимости. Эта переменная «захватывается» и остаётся доступной даже после того, как внешняя функция завершила работу.

code
def make_multiplier(factor):
    def multiply(x):
        return x * factor  # factor захвачен из внешней области
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

Переменная factor живёт в объекте функции multiply — её можно увидеть через double.__closure__. Именно этот механизм позволяет декораторам «обёртывать» функцию, сохраняя доступ к ней внутри обёртки.


Первый декоратор: разбираем по шагам

Напишем простейший декоратор — логгер вызовов:

code
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов: {func.__name__} с аргументами {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"Результат: {result}")
        return result
    return wrapper

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

add = log_calls(add)  # Явное применение декоратора
add(3, 5)

Вывод:

code
Вызов: add с аргументами (3, 5), {}
Результат: 8

Синтаксис @log_calls над функцией — это просто сокращённая запись того же самого:

code
@log_calls
def add(a, b):
    return a + b

# Эквивалентно:
# add = log_calls(add)

Никакой магии. @ — это синтаксический сахар, который Python раскрывает в момент определения функции.


Проблема потери идентичности и functools.wraps

Есть одна серьёзная проблема с декоратором выше. После применения функция add — это уже не add, а wrapper:

code
print(add.__name__)   # wrapper
print(add.__doc__)    # None

Это ломает интроспекцию, документацию, логирование и — что критично — некоторые фреймворки, которые полагаются на имена функций (например, Flask при регистрации маршрутов).

Решение — functools.wraps. Это декоратор для декоратора, который копирует метаданные оригинальной функции на обёртку:

code
import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Вызов: {func.__name__}")
        result = func(*args, **kwargs)
        return result
    return wrapper

@log_calls
def add(a, b):
    """Складывает два числа."""
    return a + b

print(add.__name__)  # add
print(add.__doc__)   # Складывает два числа.

functools.wraps копирует __name__, __qualname__, __doc__, __dict__ и __module__. Это стандарт — использовать его нужно всегда, без исключений. Пропустить functools.wraps в продакшн-коде — распространённая ошибка, которая проявляется не сразу, но больно бьёт при отладке.


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

Простые декораторы принимают функцию и возвращают функцию. Но что если декоратору нужны собственные параметры? Например, @retry(times=3) или @require_role("admin").

Здесь нужен ещё один уровень вложенности: фабрика декораторов.

code
import functools
import time

def retry(times=3, delay=1.0):
    """Повторяет вызов функции при исключении."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"Попытка {attempt}/{times} не удалась: {e}")
                    if attempt < times:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@retry(times=3, delay=0.5)
def fetch_data(url):
    # Симулируем нестабильное соединение
    import random
    if random.random() < 0.7:
        raise ConnectionError("Нет соединения")
    return f"Данные с {url}"

Структура: retry(times=3) возвращает decorator, decorator(func) возвращает wrapper. Три уровня вложенности — это нормально и читаемо, если каждый уровень несёт свою ответственность.

Ключевой момент: когда вы пишете @retry(times=3), Python сначала вызывает retry(times=3) и получает decorator, а затем применяет его к функции. Если же написать просто @retry без скобок — это уже другое: retry применится напрямую к функции, и она окажется аргументом times. Это частая ошибка при написании декораторов с опциональными аргументами.

Декоратор, работающий и со скобками, и без

Чтобы декоратор работал в обоих режимах (@retry и @retry(times=5)), можно использовать трюк с functools.partial:

code
import functools

def retry(func=None, *, times=3, delay=1.0):
    if func is None:
        # Вызван с аргументами: @retry(times=5)
        return functools.partial(retry, times=times, delay=delay)
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for attempt in range(1, times + 1):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                if attempt == times:
                    raise
                time.sleep(delay)
    return wrapper

Теперь работают оба варианта:

code
@retry
def operation_a(): ...

@retry(times=5, delay=2.0)
def operation_b(): ...

Практические паттерны

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

Стандартная библиотека уже содержит functools.lru_cache и functools.cache — но понимать, как они устроены, полезно:

code
import functools

def memoize(func):
    """Простое кэширование без ограничения размера."""
    cache = {}
    
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    
    wrapper.cache = cache  # Открываем доступ к кэшу снаружи
    wrapper.cache_clear = lambda: cache.clear()
    return wrapper

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

print(fibonacci(50))  # Мгновенно, без рекурсии в 2^50 вызовов

Обратите внимание на wrapper.cache = cache — декоратор может расширять интерфейс функции, добавляя атрибуты. Именно так устроен lru_cache, который добавляет метод cache_info().

Контроль доступа

Паттерн, который активно используется в веб-фреймворках:

code
import functools
from typing import Callable

def require_permission(permission: str):
    """Проверяет наличие разрешения у текущего пользователя."""
    def decorator(func: Callable):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # В реальном приложении current_user берётся из контекста
            current_user = get_current_user()
            if not current_user.has_permission(permission):
                raise PermissionError(
                    f"Требуется разрешение '{permission}', "
                    f"у пользователя {current_user.name} его нет"
                )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_permission("reports.export")
def export_report(report_id: int) -> bytes:
    """Экспортирует отчёт в PDF."""
    ...

Этот паттерн чист и тестируем: логику проверки прав можно покрыть юнит-тестами независимо от бизнес-логики функции.

Измерение времени выполнения

code
import functools
import time
import logging

logger = logging.getLogger(__name__)

def timeit(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            result = func(*args, **kwargs)
            return result
        finally:
            elapsed = time.perf_counter() - start
            logger.debug(
                "%s выполнилась за %.4f сек",
                func.__qualname__,
                elapsed
            )
    return wrapper

Использование time.perf_counter() вместо time.time() — правильный выбор для измерения коротких интервалов: он не зависит от системных часов и имеет более высокое разрешение.


Стекирование декораторов и порядок применения

Декораторы можно стекировать — и порядок имеет значение:

code
@decorator_a
@decorator_b
@decorator_c
def my_func():
    ...

# Эквивалентно:
# my_func = decorator_a(decorator_b(decorator_c(my_func)))

Декораторы применяются снизу вверх при определении, но выполняются сверху вниз при вызове. Это интуитивно не очевидно и является источником багов. Если decorator_a — это @timeit, а decorator_b — @retry, то timeit будет измерять время включая все повторные попытки. Поменяйте их местами — и будете измерять время одного вызова.


Декораторы классов

Декораторы применимы не только к функциям, но и к классам. Стандартный пример — @dataclass:

code
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

# Python автоматически генерирует __init__, __repr__, __eq__
p = Point(1.0, 2.0)
print(p)  # Point(x=1.0, y=2.0)

Декоратор класса получает класс как аргумент и может возвращать модифицированный класс или совершенно новый объект.


Когда декораторы — не лучший выбор

Декораторы элегантны, но не универсальны. Несколько ситуаций, где стоит остановиться:

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

Изменение сигнатуры. Декоратор, который меняет аргументы функции, ломает статический анализ и документацию. functools.wraps копирует __wrapped__, но типизаторы всё равно могут запутаться. В таких случаях лучше рассмотреть явное наследование или протоколы.

Отладка. Стек вызовов с несколькими декораторами может быть трудночитаемым. Хорошо написанный декоратор с functools.wraps и осмысленными именами обёрток снижает эту проблему, но не устраняет полностью.

Именно такие нюансы — граница между «умело применил» и «переусложнил» — разбираются в программах уровня «Senior Python», где декораторы рассматриваются в контексте реальной архитектуры приложений, а не как изолированная синтаксическая конструкция.


Декораторы и типизация

Начиная с Python 3.10, правильная типизация декораторов стала значительно проще благодаря ParamSpec и TypeVar:

code
from typing import TypeVar, Callable
from collections.abc import Callable as CallableABC
import functools

F = TypeVar("F", bound=CallableABC)

def log_calls(func: F) -> F:
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__}")
        return func(*args, **kwargs)
    return wrapper  # type: ignore[return-value]

Для декораторов с аргументами и сохранением сигнатуры используется ParamSpec:

code
from typing import ParamSpec, TypeVar, Callable
import functools

P = ParamSpec("P")
R = TypeVar("R")

def log_calls(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"Вызов {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

Теперь mypy и Pyright понимают, что декорированная функция сохраняет оригинальную сигнатуру. Это важно в больших кодовых базах, где статический анализ — часть CI/CD.


Итог

Декораторы в Python — это не просто удобный синтаксис. Это полноценный инструмент для разделения ответственности: бизнес-логика функции остаётся чистой, а сквозные задачи (логирование, кэширование, авторизация, повторные попытки) выносятся в переиспользуемые обёртки.

Ключевые вещи, которые стоит держать в голове:

  • Декоратор — это функция, возвращающая функцию. Замыкание — механизм, который это обеспечивает.
  • functools.wraps — не опциональная деталь, а обязательный элемент любого продакшн-декоратора.
  • Декораторы с аргументами требуют дополнительного уровня вложенности — фабрики декораторов.
  • Порядок стекирования декораторов влияет на поведение и требует осознанного выбора.
  • Правильная типизация с ParamSpec делает декораторы совместимыми со статическим анализом.

Если хочется разобраться глубже — в том числе понять, как декораторы вписываются в более широкие паттерны Python-разработки, — курс «Senior Python» охватывает эти темы в контексте реальных задач: от архитектурных решений до тонкостей работы интерпретатора.

Декораторы — один из тех инструментов, которые отличают код, написанный «чтобы работало», от кода, написанного «чтобы поддерживалось».

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Senior Python

Курс по теме

Senior Python

Senior Python: классы под микроскопом — погружение во внутреннее устройство CPython для middle/senior: от декораторов и метаклассов до байткода и GC. Разбор исходников языка, трассировки и тесты к каждому уроку.

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Приложения для iPhone и Apple Watch на SwiftUI

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Приложения для macOS на SwiftUI

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость
python

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость

Поймём, где чаще всего ломается воспроизводимость (версии данных, фильтры, дрейф) и как оформить пайплайн признаков так, чтобы результаты совпадали между запусками и окружениями.

17 сентября 2026 г.
1040
Системный дебаг памяти и производительности в Python: где искать утечки и как проверять гипотезы
системный

Системный дебаг памяти и производительности в Python: где искать утечки и как проверять гипотезы

Пошагово разберём типовые причины роста памяти (кэширование, циклические ссылки, удержание объектов в замыканиях, неверная работа с ресурсами) и как подтверждать догадки трейсами, профилировщиками и метриками. В конце — чек-лист расследования для задач из

15 сентября 2026 г.
1030
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1421
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1070
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

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

2 октября 2026 г.
620

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Функции как объекты первого классаЗамыкания: механизм, на котором всё держитсяПервый декоратор: разбираем по шагамПроблема потери идентичности и functools.wrapsДекораторы с аргументамиДекоратор, работающий и со скобками, и безПрактические паттерныКэширование результатовКонтроль доступаИзмерение времени выполненияСтекирование декораторов и порядок примененияДекораторы классовКогда декораторы — не лучший выборДекораторы и типизацияИтог