Декоратор в Python это обычная функция, которая принимает другую функцию (или класс) как аргумент, расширяет её поведение и возвращает новый вызываемый объект. Синтаксис @decorator это просто синтаксический сахар: запись @timer над функцией foo полностью эквивалентна foo = timer(foo).
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 = my_decorator(greet)
greet("Alice")
# до вызова
# Привет, Alice!
# после вызова
Механизм работы в три шага: Python видит @decorator, вызывает decorator(func) в момент определения функции (не при вызове), сохраняет результат под тем же именем. Это важно: код декоратора вне wrapper выполняется один раз при загрузке модуля.
functools.wraps — первое правило декораторов
Без @wraps задекорированная функция теряет своё имя, docstring и сигнатуру. Это ломает отладку, help(), inspect, Sphinx-документацию и любые инструменты интроспекции.
from functools import wraps
def my_decorator(func):
@wraps(func) # копирует __name__, __doc__, __module__, __annotations__
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a: int, b: int) -> int:
"""Складывает два числа."""
return a + b
print(add.__name__) # add (без @wraps было бы 'wrapper')
print(add.__doc__) # Складывает два числа.
@wraps под капотом вызывает functools.update_wrapper, который копирует атрибуты __module__, __name__, __qualname__, __annotations__, __doc__ и обновляет __wrapped__, позволяя при необходимости добраться до оригинальной функции через add.__wrapped__.
Встроенные декораторы — стандартная библиотека
@property
Превращает метод в управляемый атрибут с геттером, сеттером и делитером. Позволяет добавить валидацию без изменения публичного API класса.
class Temperature:
def __init__(self, celsius: float = 0):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float):
if value < -273.15:
raise ValueError("Ниже абсолютного нуля невозможно")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
t = Temperature(100)
print(t.fahrenheit) # 212.0
t.celsius = -300 # ValueError
@staticmethod и @classmethod
@staticmethod объявляет метод, не привязанный ни к экземпляру (self), ни к классу (cls
это просто функция внутри пространства имён класса. @classmethod передаёт сам класс первым аргументом, что позволяет создавать альтернативные конструкторы.
class Date:
def __init__(self, year: int, month: int, day: int):
self.year, self.month, self.day = year, month, day
@classmethod
def from_string(cls, s: str) -> "Date":
year, month, day = map(int, s.split("-"))
return cls(year, month, day) # работает и в подклассах
@staticmethod
def is_leap_year(year: int) -> bool:
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
d = Date.from_string("2024-03-15")
print(Date.is_leap_year(2024)) # True
@dataclass
Автоматически генерирует __init__, __repr__, __eq__ и, опционально, методы сравнения и хэширования. Начиная с Python 3.10 можно использовать slots=True для экономии памяти.
from dataclasses import dataclass, field
@dataclass(order=True, frozen=True)
class Point:
x: float
y: float
label: str = field(default="", compare=False)
p1 = Point(1.0, 2.0, "A")
p2 = Point(3.0, 4.0, "B")
print(p1 < p2) # True (сравнивает x, затем y)
# p1.x = 5.0 # FrozenInstanceError, т.к. frozen=True
Параметры eq, order, frozen, slots, kw_only (3.10+) и match_args (3.10+) позволяют точно настроить поведение.
@abstractmethod
Из модуля abc. Помечает метод как абстрактный: класс, содержащий хотя бы один такой метод, нельзя инстанциировать. Подкласс обязан переопределить все абстрактные методы.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
class Circle(Shape):
def __init__(self, r: float):
self.r = r
def area(self) -> float:
return 3.14159 * self.r ** 2
def perimeter(self) -> float:
return 2 * 3.14159 * self.r
# Shape() # TypeError: Can't instantiate abstract class
c = Circle(5)
@cached_property
Добавлен в Python 3.8. Вычисляет значение при первом обращении, кэширует его в __dict__ экземпляра и больше не вызывает getter. В отличие от @property нет лишнего вызова при каждом обращении.
from functools import cached_property
import statistics
class Dataset:
def __init__(self, data: list[float]):
self._data = data
@cached_property
def stats(self) -> dict:
print("вычисляем...") # выполнится только один раз
return {
"mean": statistics.mean(self._data),
"stdev": statistics.stdev(self._data),
}
ds = Dataset([1, 2, 3, 4, 5])
print(ds.stats) # вычисляем... {'mean': 3, 'stdev': 1.58...}
print(ds.stats) # из кэша, без "вычисляем..."
Важно: не работает с __slots__ и не потокобезопасен без внешней блокировки.
@override (Python 3.12+)
Из модуля typing. Сигнализирует тайп-чекеру, что метод переопределяет родительский. Если в родителе метод переименовали или удалили, тайп-чекер выдаст ошибку.
from typing import override
class Base:
def process(self, data: str) -> str:
return data.upper()
class Child(Base):
@override
def process(self, data: str) -> str: # ошибка, если в Base нет process
return data.lower()
@deprecated (Python 3.13+)
Из модуля warnings. Помечает функцию или класс как устаревший: при вызове автоматически выдаётся DeprecationWarning, а тайп-чекеры отображают предупреждение.
from warnings import deprecated
@deprecated("Используйте new_api() вместо этого")
def old_api():
return 42
old_api() # DeprecationWarning: Используйте new_api() вместо этого
functools — арсенал высшего порядка
@lru_cache и @cache
@lru_cache(maxsize=N) кэширует результаты функции по аргументам (мемоизация), выбрасывая наименее использованные записи при достижении лимита. @cache (Python 3.9+) это @lru_cache(maxsize=None) без ограничения размера.
from functools import lru_cache, cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # мгновенно
print(fibonacci.cache_info()) # CacheInfo(hits=48, misses=51, maxsize=128, currsize=51)
fibonacci.cache_clear() # сбросить кэш
# Для чистых функций без ограничения размера:
@cache
def factorial(n: int) -> int:
return n * factorial(n - 1) if n else 1
Аргументы должны быть хэшируемыми. Список, словарь или другой изменяемый тип вызовет TypeError. Для методов экземпляра лучше использовать @cached_property или явно кэшировать через словарь.
@singledispatch
Реализует перегрузку функций по типу первого аргумента (single dispatch, аналог pattern matching по типу).
from functools import singledispatch
@singledispatch
def process(value):
raise TypeError(f"Неподдерживаемый тип: {type(value)}")
@process.register(int)
def _(value: int) -> str:
return f"Целое: {value * 2}"
@process.register(str)
def _(value: str) -> str:
return f"Строка: {value.upper()}"
@process.register(list)
@process.register(tuple)
def _(value) -> str:
return f"Последовательность длиной {len(value)}"
print(process(5)) # Целое: 10
print(process("hello")) # Строка: HELLO
print(process([1, 2, 3])) # Последовательность длиной 3
Для методов классов используется functools.singledispatchmethod (Python 3.8+).
@total_ordering
Если определить __eq__ и один из методов сравнения (__lt__, __le__, __gt__, __ge__), @total_ordering автоматически выведет остальные три.
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major: int, minor: int, patch: int):
self.v = (major, minor, patch)
def __eq__(self, other) -> bool:
return self.v == other.v
def __lt__(self, other) -> bool:
return self.v < other.v
v1 = Version(1, 2, 3)
v2 = Version(2, 0, 0)
print(v1 < v2) # True
print(v1 >= v2) # False (сгенерировано @total_ordering)
print(v2 > v1) # True (сгенерировано @total_ordering)
Декораторы с параметрами
Чтобы передать аргументы в декоратор, нужен ещё один уровень вложенности: функция, возвращающая декоратор.
from functools import wraps
import time
def retry(max_attempts: int = 3, delay: float = 1.0, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exc = e
print(f"Попытка {attempt}/{max_attempts} не удалась: {e}")
if attempt < max_attempts:
time.sleep(delay)
raise last_exc
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError,))
def fetch_data(url: str) -> str:
# симулируем нестабильное соединение
import random
if random.random() < 0.7:
raise ConnectionError("Нет соединения")
return "data"
Начиная с Python 3.8 можно сделать декоратор, работающий как с аргументами (@retry(3)), так и без (@retry), через параметр func=None и позиционно-ключевые аргументы:
def retry(_func=None, *, max_attempts: int = 3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception:
pass
raise RuntimeError("Все попытки исчерпаны")
return wrapper
if _func is not None: # вызов без скобок: @retry
return decorator(_func)
return decorator # вызов со скобками: @retry(max_attempts=5)
Стекирование декораторов
Несколько декораторов применяются снизу вверх: ближайший к функции оборачивает первым.
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name: str) -> str:
return f"Привет, {name}!"
# Эквивалентно: greet = bold(italic(greet))
print(greet("Alice")) # <b><i>Привет, Alice!</i></b>
Порядок имеет значение. @bold @italic дают <b><i>...</i></b>, тогда как @italic @bold дадут <i><b>...</b></i>.
Декораторы классов
Декоратор может быть не только функцией, но и классом. В таком случае __call__ становится телом обёртки, а __init__ получает декорируемую функцию.
from functools import wraps, update_wrapper
import time
class Timer:
"""Декоратор-класс: замеряет время выполнения."""
def __init__(self, func):
self.func = func
self.total_time = 0.0
self.call_count = 0
update_wrapper(self, func) # аналог @wraps для классов
def __call__(self, *args, **kwargs):
start = time.perf_counter()
result = self.func(*args, **kwargs)
elapsed = time.perf_counter() - start
self.total_time += elapsed
self.call_count += 1
print(f"{self.func.__name__}: {elapsed:.4f}с")
return result
def stats(self):
avg = self.total_time / self.call_count if self.call_count else 0
return {"total": self.total_time, "calls": self.call_count, "avg": avg}
@Timer
def slow_sum(n: int) -> int:
return sum(range(n))
slow_sum(1_000_000)
slow_sum(500_000)
print(slow_sum.stats())
Декоратор-класс удобен, когда нужно хранить состояние между вызовами (счётчик, накопленное время, кэш).
Применение декоратора к классу целиком
Декоратор можно применить и к классу: Python передаёт объект класса, а не функции.
def add_repr(cls):
"""Добавляет __repr__, если его нет."""
if "__repr__" not in cls.__dict__:
def __repr__(self):
attrs = ", ".join(
f"{k}={v!r}" for k, v in self.__dict__.items()
)
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
@add_repr
class Config:
def __init__(self, host: str, port: int):
self.host = host
self.port = port
print(Config("localhost", 8080)) # Config(host='localhost', port=8080)
Практические паттерны
Логирование
import logging
from functools import wraps
def log_calls(logger=None, level=logging.DEBUG):
_logger = logger or logging.getLogger(__name__)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
_logger.log(level, "Вызов %s args=%s kwargs=%s", func.__name__, args, kwargs)
try:
result = func(*args, **kwargs)
_logger.log(level, "%s вернул %r", func.__name__, result)
return result
except Exception as exc:
_logger.exception("%s выбросил %s", func.__name__, exc)
raise
return wrapper
return decorator
logging.basicConfig(level=logging.DEBUG)
@log_calls()
def divide(a: float, b: float) -> float:
return a / b
Rate Limiter
import time
from collections import deque
from functools import wraps
def rate_limit(calls: int, period: float):
"""Не более `calls` вызовов за `period` секунд."""
def decorator(func):
timestamps: deque = deque()
@wraps(func)
def wrapper(*args, **kwargs):
now = time.monotonic()
while timestamps and now - timestamps >= period:
timestamps.popleft()
if len(timestamps) >= calls:
sleep_for = period - (now - timestamps)
time.sleep(sleep_for)
timestamps.append(time.monotonic())
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(calls=5, period=1.0)
def call_api(endpoint: str) -> dict:
return {"endpoint": endpoint, "ok": True}
Singleton
from functools import wraps
import threading
def singleton(cls):
instances = {}
lock = threading.Lock()
@wraps(cls, updated=[])
def get_instance(*args, **kwargs):
if cls not in instances:
with lock:
if cls not in instances: # double-checked locking
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabasePool:
def __init__(self, dsn: str = "sqlite:///:memory:"):
self.dsn = dsn
print(f"Создан пул: {dsn}")
a = DatabasePool("postgresql://localhost/db")
b = DatabasePool() # "Создан пул" не печатается второй раз
print(a is b) # True
Валидация типов
import inspect
from functools import wraps
def validate_types(func):
sig = inspect.signature(func)
hints = func.__annotations__
@wraps(func)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for name, value in bound.arguments.items():
if name in hints and name != "return":
expected = hints[name]
if not isinstance(value, expected):
raise TypeError(
f"Параметр '{name}': ожидался {expected.__name__}, "
f"получен {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
@validate_types
def power(base: float, exp: int) -> float:
return base ** exp
power(2.0, 3) # OK
power(2.0, 3.5) # TypeError: Параметр 'exp': ожидался int, получен float
Асинхронные декораторы
Декоратор не знает заранее, синхронная функция или async. Для универсального декоратора нужно проверять это явно.
import asyncio
import time
import inspect
from functools import wraps
def timeit(func):
@wraps(func)
async def async_wrapper(*args, **kwargs):
start = time.perf_counter()
result = await func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.4f}с (async)")
return result
@wraps(func)
def sync_wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.4f}с (sync)")
return result
# Выбираем обёртку один раз при декорировании (не при каждом вызове)
return async_wrapper if inspect.iscoroutinefunction(func) else sync_wrapper
@timeit
def cpu_task(n: int) -> int:
return sum(range(n))
@timeit
async def io_task(delay: float) -> str:
await asyncio.sleep(delay)
return "done"
cpu_task(10_000_000)
asyncio.run(io_task(0.1))
Проверка inspect.iscoroutinefunction выполняется один раз при декорировании, а не при каждом вызове, что исключает лишние расходы в рантайме.
Декораторы в контексте Python 3.13 и 3.14
В Python 3.13 появился @typing.deprecated для явной пометки устаревшего кода прямо в системе типов. В Python 3.14 аннотации стали отложенными (PEP 649): они не вычисляются при определении функции. Это означает, что декораторы, читающие func.__annotations__ напрямую (например, validate_types выше), теперь получат объект AnnotateFunc, а не уже вычисленный словарь. Для надёжной работы с аннотациями используйте inspect.get_annotations(func, eval_str=True):
import inspect
def validate_types(func):
sig = inspect.signature(func)
# eval_str=True вычисляет строковые аннотации и отложенные (3.14+)
hints = inspect.get_annotations(func, eval_str=True)
...
Шпаргалка — когда что использовать
| Задача |
Декоратор |
| Управляемый атрибут |
@property |
Метод без self/cls |
@staticmethod |
| Альтернативный конструктор |
@classmethod |
| Автогенерация boilerplate |
@dataclass |
| Абстрактный интерфейс |
@abstractmethod |
| Ленивое кэширование атрибута |
@cached_property |
| Мемоизация чистых функций |
@lru_cache / @cache |
| Перегрузка по типу |
@singledispatch |
| Вывод методов сравнения |
@total_ordering |
| Пометить как устаревший |
@deprecated (3.13+) |
| Явное переопределение метода |
@override (3.12+) |
| Сохранить метаданные функции |
@wraps |
Полезные источники
References
-
Fancy Decorators - In this tutorial, you’ll look at what Python decorators are and how you define and use them. Decorat…
-
The Python Decorator Handbook - freeCodeCamp - Python decorators provide an easy yet powerful syntax for modifying and extending the behavior of fu…
-
functools — Higher-order functions and operations on callable … - The functools module is for higher-order functions: functions that act on or return other functions…
-
Python Standard Library | collections | itertools | functools - Dev Genius - Python’s functools module, part of the standard library, provides higher-order functions and operati…
-
Handy Python Decorators. Implementing and Using Advanced… - The Python Standard Library contains four incredibly useful decorators: staticmethod , classmethod ,…
-
What’s New In Python 3.13 — Python 3.14.4 documentation - The biggest changes include a new interactive interpreter, experimental support for running in a fre…
-
What’s New In Python 3.13 — Python 3.14.0rc3 documentation - Editors, Adam Turner and Thomas Wouters,. This article explains the new features in Python 3.13, com…
-
Mastering Python Decorators for Code Reusability and Optimization - Discover how Python decorators can help you write reusable, efficient, and maintainable code by modi…
-
The Comprehensive Guide to Python Decorators - Decorators are one of Python’s most powerful metaprogramming tools, widely used in the industry to w…
-
What’s new in Python 3.14 — Python 3.14.4 documentation - This article explains the new features in Python 3.14, compared to 3.13. … no_type_check_decorator…