Декораторы в Python — что это и как применять на практике
Что такое декоратор
Декоратор — это функция, которая принимает другую функцию и возвращает новую, расширенную версию. Звучит абстрактно, но на деле это способ «обернуть» функцию дополнительной логикой — замерить время выполнения, добавить логирование, кэшировать результат — не меняя исходный код.
В Python функции — объекты первого класса: их можно передавать как аргументы, возвращать из других функций и сохранять в переменные. Именно на этом строится механизм декораторов.
Как декоратор работает изнутри
Разберём шаг за шагом. Допустим, мы хотим замерить время выполнения функции:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} выполнилась за {elapsed:.3f} с")
return result
return wrapperЧто здесь происходит:
timerпринимает функциюfunc.- Внутри создаётся функция-обёртка
wrapper, которая вызываетfunc, но добавляет логику до и после. timerвозвращаетwrapper— новую функцию с той же сигнатурой.
Теперь применяем:
def load_data(path):
# долгая загрузка данных
import pandas as pd
return pd.read_csv(path)
load_data = timer(load_data) # «оборачиваем» вручную
df = load_data("events.csv")
# load_data выполнилась за 2.341 сСтрока load_data = timer(load_data) — и есть декорирование. Вместо неё Python предлагает синтаксический сахар.
Синтаксис @
Запись @decorator перед определением функции — эквивалент присваивания func = decorator(func):
@timer
def load_data(path):
import pandas as pd
return pd.read_csv(path)
# То же самое, что load_data = timer(load_data)Короче, чище, нагляднее. Можно применить несколько декораторов — они выполняются снизу вверх:
@decorator_a
@decorator_b
def func():
pass
# Эквивалент: func = decorator_a(decorator_b(func))functools.wraps — сохраняем метаданные
Без functools.wraps обёрнутая функция теряет своё имя и docstring:
@timer
def load_data(path):
"""Загружает данные из CSV."""
...
print(load_data.__name__) # 'wrapper' — не то, что хотели
print(load_data.__doc__) # NoneРешение — functools.wraps:
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.time() - start:.3f}s")
return result
return wrapper
@timer
def load_data(path):
"""Загружает данные из CSV."""
...
print(load_data.__name__) # 'load_data' ✓
print(load_data.__doc__) # 'Загружает данные из CSV.' ✓Правило: всегда используйте @functools.wraps(func) в обёртке. На собеседовании часто спрашивают, зачем это нужно.
Практические примеры
Логирование вызовов
import functools
import logging
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Вызов {func.__name__}, args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logging.info(f"{func.__name__} вернула {result}")
return result
return wrapper
@log_call
def calc_retention(cohort_size, returned):
return returned / cohort_size if cohort_size else 0Retry — повторные попытки
import functools
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"{func.__name__}: попытка {attempt} не удалась, повтор через {delay}с")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=2)
def fetch_api_data(url):
import requests
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()Обратите внимание: retry — не сам декоратор, а фабрика декораторов. Она принимает параметры и возвращает настоящий декоратор. Это частый паттерн.
Кэширование (мемоизация)
import functools
@functools.lru_cache(maxsize=128)
def expensive_calculation(n):
"""Считает что-то долго, результат кэшируется."""
return sum(i ** 2 for i in range(n))
expensive_calculation(10000) # считает
expensive_calculation(10000) # берёт из кэшаlru_cache — встроенный декоратор из functools. Кэширует результаты вызовов по аргументам. Полезен для чистых функций (без побочных эффектов) с дорогими вычислениями.
Встроенные декораторы
@property — свойства
Позволяет обращаться к методу как к атрибуту:
class Experiment:
def __init__(self, control, treatment):
self.control = control
self.treatment = treatment
@property
def lift(self):
ctrl_mean = sum(self.control) / len(self.control)
treat_mean = sum(self.treatment) / len(self.treatment)
return (treat_mean - ctrl_mean) / ctrl_mean
exp = Experiment([100, 110, 95], [120, 115, 130])
print(exp.lift) # 0.213... — без скобок, как атрибут@staticmethod и @classmethod
class DataValidator:
VALID_TYPES = {"int", "float", "str", "bool"}
@staticmethod
def is_positive(value):
"""Не нужен доступ к экземпляру или классу."""
return value > 0
@classmethod
def from_config(cls, config_dict):
"""Альтернативный конструктор — доступ к классу через cls."""
return cls(**config_dict)@staticmethod — обычная функция внутри класса, не получает self и cls. @classmethod — получает класс первым аргументом, используется для альтернативных конструкторов.
Подробнее про lambda-функции — ещё один способ создания компактных функций.
Декораторы для классов
Декорировать можно не только функции, но и классы. Самый частый пример — @dataclass:
from dataclasses import dataclass
@dataclass
class QueryResult:
query: str
rows: int
execution_time: floatДекоратор @dataclass принимает класс и возвращает модифицированную версию с автоматически созданными __init__, __repr__, __eq__.
Вопросы с собеседований
— Что такое декоратор в Python?
— Функция, которая принимает функцию и возвращает новую функцию с расширенным поведением. Синтаксис @decorator — сахар для func = decorator(func). Используется для кросс-функциональной логики: логирование, кэширование, замер времени, контроль доступа.
— Зачем нужен functools.wraps?
— Без wraps обёрнутая функция теряет __name__, __doc__ и другие атрибуты — у неё будет имя обёртки (wrapper). @functools.wraps(func) копирует метаданные оригинала в обёртку. Это важно для отладки, документации и интроспекции.
— Как написать декоратор с параметрами?
— Нужна фабрика декораторов — функция, которая принимает параметры и возвращает сам декоратор. Три уровня вложенности: фабрика → декоратор → обёртка. Пример: @retry(max_attempts=3) — retry принимает параметр и возвращает декоратор.
— Чем @staticmethod отличается от @classmethod?
— @staticmethod не получает ни self, ни cls — это обычная функция, привязанная к пространству имён класса. @classmethod получает класс (cls) первым аргументом — полезен для альтернативных конструкторов и работы с наследованием.
FAQ
Декораторы замедляют код?
Минимально. Каждый вызов декорированной функции — это вызов обёртки плюс вызов оригинала. Накладные расходы — микросекунды. Для аналитических задач (обработка данных, SQL-запросы) это ничтожно мало. Не оптимизируйте то, что не является узким местом.
Когда аналитику стоит писать свои декораторы?
Когда одна и та же обвязка повторяется у многих функций: замер времени, логирование, повторные попытки при ошибках API, валидация входных данных. Если логика уникальна для одной функции — декоратор не нужен, просто напишите код внутри неё. Больше практик — в гайде по Python для аналитика.
Можно ли применить несколько декораторов к одной функции?
Да. Декораторы применяются снизу вверх: ближайший к def оборачивает первым, верхний — последним. Порядок важен. Например, @timer поверх @retry замерит общее время включая повторы, а в обратном порядке — время каждой попытки отдельно.
Потренируйте вопросы по Python на реальных задачах — откройте тренажёр. 1500+ вопросов, которые спрашивают на собеседованиях аналитика. Бесплатно.