Полезные декораторы в Python — исчерпывающий гайд
-
Декоратор в 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 / bRate 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
Полезные источники
- Официальная документация
functools— docs.python.org/3/library/functools.html - “What’s New In Python 3.13” — docs.python.org/3/whatsnew/3.13.html (про
@deprecated,@override) - “What’s New In Python 3.14” — docs.python.org/3/whatsnew/3.14.html (про отложенные аннотации PEP 649)
- Real Python — Primer on Python Decorators — realpython.com/primer-on-python-decorators/
- freeCodeCamp — The Python Decorator Handbook — freecodecamp.org/news/the-python-decorator-handbook/
- asyncmove.com — The Comprehensive Guide to Python Decorators (2026) — asyncmove.com/blog/2026/01/the-comprehensive-guide-to-python-decorators/
- PEP 318 — декораторы функций и методов
- PEP 614 — расслабление грамматических ограничений на декораторы (3.9+)
- PEP 649 — отложенные аннотации (3.14)
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…
- Официальная документация
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.