Перейти к содержанию
  • Лента
  • Категории
  • Последние
  • Метки
  • Популярные
  • Пользователи
  • Группы
Свернуть
exlends
Категории
  1. Главная
  2. Категории
  3. Языки программирования
  4. Python
  5. Полезные декораторы в Python — исчерпывающий гайд

Полезные декораторы в Python — исчерпывающий гайд

Запланировано Прикреплена Закрыта Перенесена Python
1 Сообщения 1 Постеры 29 Просмотры
  • Сначала старые
  • Сначала новые
  • По количеству голосов
Ответить
  • Ответить, создав новую тему
Авторизуйтесь, чтобы ответить
Эта тема была удалена. Только пользователи с правом управления темами могут её видеть.
  • AladdinA Не в сети
    AladdinA Не в сети
    Aladdin
    js
    написал отредактировано Aladdin
    #1

    Декоратор в 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

    Полезные источники

    • Официальная документация 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

    1. Fancy Decorators - In this tutorial, you’ll look at what Python decorators are and how you define and use them. Decorat…

    2. The Python Decorator Handbook - freeCodeCamp - Python decorators provide an easy yet powerful syntax for modifying and extending the behavior of fu…

    3. functools — Higher-order functions and operations on callable … - The functools module is for higher-order functions: functions that act on or return other functions…

    4. Python Standard Library | collections | itertools | functools - Dev Genius - Python’s functools module, part of the standard library, provides higher-order functions and operati…

    5. Handy Python Decorators. Implementing and Using Advanced… - The Python Standard Library contains four incredibly useful decorators: staticmethod , classmethod ,…

    6. 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…

    7. 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…

    8. Mastering Python Decorators for Code Reusability and Optimization - Discover how Python decorators can help you write reusable, efficient, and maintainable code by modi…

    9. The Comprehensive Guide to Python Decorators - Decorators are one of Python’s most powerful metaprogramming tools, widely used in the industry to w…

    10. 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…

    1 ответ Последний ответ
    0

    Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.

    Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.

    С вашими комментариями этот пост мог бы стать ещё лучше 💗

    Зарегистрироваться Войти

    Категории

    • Главная
    • Новости
    • Фронтенд
    • Бекенд
    • Языки программирования

    Контакты

    • Сотрудничество
    • info@exlends.com

    © 2024 - 2026 ExLends, Inc. Все права защищены.

    Политика конфиденциальности
    • Войти

    • Нет учётной записи? Зарегистрироваться

    • Войдите или зарегистрируйтесь для поиска.
    • Первое сообщение
      Последнее сообщение
    0
    • Лента
    • Категории
    • Последние
    • Метки
    • Популярные
    • Пользователи
    • Группы