Python: уроки, справочник и задачи

Декораторы в Python: синтаксис, параметры и примеры

Раздел: Функции Python · Обновлено 31.10.2026

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

Декораторы в Python
Фото: W.carter / CC BY-SA 4.0, Wikimedia Commons
Содержание статьи
  1. Что такое декоратор
  2. Как написать свой декоратор
    1. Зачем нужен functools.wraps
  3. Декоратор с параметрами
  4. Несколько декораторов на одной функции
  5. Декораторы для методов и классов
    1. Встроенные декораторы
  6. Практические примеры
    1. Замер времени выполнения
    2. Повтор при ошибке
  7. Частые вопросы
    1. Когда выполняется код декоратора?
    2. Можно ли снять декоратор с функции?
    3. Чем декоратор отличается от замыкания?
    4. Может ли декоратор быть классом?
    5. Почему функция после декоратора возвращает None?

Что такое декоратор

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

Запись с символом @ над определением функции - это сокращение. Две формы ниже делают одно и то же:

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

# то же самое без синтаксиса @
def say_hello():
    print("Привет")
say_hello = my_decorator(say_hello)

Декоратор применяется один раз, в момент определения функции, то есть при импорте модуля. Дальше при каждом вызове say_hello() выполняется уже обертка. Механизм опирается на замыкания: внутренняя функция запоминает переменную func из внешней.

Как написать свой декоратор

Классическая схема состоит из трех шагов: внешняя функция принимает func, внутри объявляется wrapper, внешняя функция возвращает wrapper без вызова.

def my_decorator(func):
    def wrapper():
        print("До вызова")
        func()
        print("После вызова")
    return wrapper

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

say_hello()
# До вызова
# Привет, мир!
# После вызова

У такой обертки два недостатка. Она не принимает аргументы и теряет результат исходной функции. Универсальный вариант использует *args и **kwargs (подробнее в статье про аргументы функции) и обязательно возвращает результат:

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__}{args}")
        result = func(*args, **kwargs)
        print(f"Результат: {result}")
        return result
    return wrapper

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

add(3, 5)
# Вызов add(3, 5)
# Результат: 8

Если забыть return result, декорированная функция будет всегда возвращать None. Это самая частая ошибка у тех, кто пишет первый декоратор.

Зачем нужен functools.wraps

После декорирования имя функции, ее docstring и аннотации подменяются атрибутами обертки. В примере выше add.__name__ вернет строку "wrapper", что мешает отладке, логированию и автодокументации. Декоратор functools.wraps копирует метаданные исходной функции в обертку:

import functools

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

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

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

Кроме того, wraps сохраняет ссылку на оригинал в атрибуте __wrapped__, поэтому исходную функцию можно вызвать в тестах напрямую. Ставьте @functools.wraps(func) в каждый декоратор, это правило хорошего тона.

Декоратор с параметрами

Когда декоратору нужны настройки, например число повторов, добавляется еще один уровень вложенности. Запись @repeat(3) сначала вызывает repeat(3), а уже возвращенный объект применяется к функции.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hello, {name}")

greet("John")  # строка выводится три раза

Получается фабрика декораторов: внешняя функция хранит параметры, средняя принимает функцию, внутренняя выполняет работу. Писать @repeat без скобок здесь нельзя, иначе в times попадет сама функция.

Несколько декораторов на одной функции

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

def bold(func):
    return lambda: "<b>" + func() + "</b>"

def upper(func):
    return lambda: func().upper()

@bold
@upper
def text():
    return "hello"

print(text())  # <b>HELLO</b>

Запись эквивалентна text = bold(upper(text)). Если поменять декораторы местами, получится <B>HELLO</B>: upper переведет в верхний регистр и сами теги. Например, кеширующий декоратор обычно ставят ближе к функции, а логирующий снаружи.

Декораторы для методов и классов

Обертка из примера с *args подходит и для методов: первым позиционным аргументом в нее придет self. Отдельно обрабатывать его не нужно, если не требуется доступ к экземпляру.

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

PLUGINS = {}

def register(cls):
    PLUGINS[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(PLUGINS)  # {'CsvExporter': <class '__main__.CsvExporter'>}

Встроенные декораторы

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

ДекораторГде находитсяЧто делает
@propertyвстроенныйПревращает метод в атрибут только для чтения, можно добавить setter
@classmethodвстроенныйМетод получает класс cls вместо экземпляра
@staticmethodвстроенныйМетод без self и cls
@functools.cachefunctoolsЗапоминает результаты вызовов без ограничения размера
@functools.lru_cache(maxsize=128)functoolsКеш с вытеснением давно не используемых значений
@dataclassdataclassesГенерирует __init__, __repr__, __eq__ по аннотациям полей

Подробные разборы есть в статьях про property и dataclasses.

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

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

Замер времени выполнения

Для измерения длительности используйте time.perf_counter(): это монотонный таймер с высокой точностью.

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__}: {time.perf_counter() - start:.4f} с")
        return result
    return wrapper

@timer
def compute(n):
    return sum(range(n))

compute(1_000_000)

Повтор при ошибке

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

import functools
import time

def retry(attempts=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(1, attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if i == attempts:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

Параметр exceptions ограничивает список ошибок, при которых есть смысл повторять вызов. Ловить все исключения подряд опасно: повторы скроют настоящую ошибку в коде.

Частые вопросы

Короткие ответы на вопросы, которые возникают при изучении декораторов.

Когда выполняется код декоратора?

Внешняя часть декоратора выполняется один раз при определении функции, то есть при импорте модуля. Код внутри wrapper выполняется при каждом вызове декорированной функции.

Можно ли снять декоратор с функции?

Если в декораторе использован functools.wraps, исходная функция доступна через атрибут __wrapped__. Без wraps добраться до нее штатно нельзя.

Чем декоратор отличается от замыкания?

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

Может ли декоратор быть классом?

Да. Класс с методом __call__ работает как декоратор: в __init__ он получает функцию, а __call__ выполняется при каждом вызове. Такой вариант удобен, когда декоратору нужно хранить состояние, например счетчик вызовов.

Почему функция после декоратора возвращает None?

Обертка не вернула результат. Добавьте return func(*args, **kwargs) или сохраните результат в переменную и верните его в конце wrapper.