Декораторы с параметрами, wraps и lru_cache

⏱ 30 мин · 🧩 заданий: 5

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

@retry(times=3) — это вызов функции retry(times=3), которая возвращает декоратор. Получается три уровня:

import functools

def retry(times=3, exceptions=(Exception,)):         # 1. фабрика: принимает параметры
    def decorator(func):                              # 2. декоратор: принимает функцию
        @functools.wraps(func)
        def wrapper(*args, **kwargs):                 # 3. обёртка: принимает аргументы вызова
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    print(f"попытка {attempt} неудачна: {e}")
                    if attempt == times:
                        raise
        return wrapper
    return decorator

calls = {"n": 0}

@retry(times=4, exceptions=(ConnectionError,))
def fetch():
    calls["n"] += 1
    if calls["n"] < 3:
        raise ConnectionError("таймаут")
    return "данные"

print(fetch())
@retry(times=4) ← retry(times=4) возвращает decorator def fetch(): ... ← fetch = decorator(fetch) = wrapper fetch() ← вызывается wrapper()

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

Иногда хочется писать и @log, и @log(level="DEBUG"). Трюк — проверить, передали ли функцию первым аргументом:

import functools

def log(func=None, *, level="INFO"):
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            print(f"[{level}] {f.__name__}{args}")
            return f(*args, **kwargs)
        return wrapper
    if func is None:          # вызвали как @log(level=...)
        return decorator
    return decorator(func)    # вызвали как @log

@log
def a(x):
    return x

@log(level="DEBUG")
def b(x):
    return x

a(1); b(2)

Кеширование: lru_cache и cache

Мемоизация — запоминание результатов функции для уже встречавшихся аргументов:

from functools import lru_cache
import time

def fib_slow(n):
    return n if n < 2 else fib_slow(n - 1) + fib_slow(n - 2)

@lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

t = time.perf_counter(); print(fib_slow(25), f"{time.perf_counter() - t:.3f} c без кеша")
t = time.perf_counter(); print(fib(25), f"{time.perf_counter() - t:.6f} c с кешем")
print(fib(300))
print(fib.cache_info())
  • @lru_cache(maxsize=128) хранит последние 128 результатов, вытесняя давно не использованные (Least Recently Used).
  • @functools.cache (3.9+) — то же, что lru_cache(maxsize=None).
  • Аргументы должны быть хешируемыми.
  • Только для чистых функций: результат зависит лишь от аргументов, нет побочных эффектов.

lru_cache на методах

@lru_cache на методе экземпляра держит ссылку на self в кеше — объекты не освобождаются из памяти. Для методов используйте functools.cached_property или кеш на уровне экземпляра.

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

Частое задание на собеседовании — реализовать мемоизацию вручную:

import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    wrapper.cache = cache
    return wrapper

@memoize
def slow_square(x):
    print(f"  считаю {x}²")
    return x * x

print(slow_square(4), slow_square(4), slow_square(5))
print(slow_square.cache)

Декоратор-класс

Декоратором может быть любой вызываемый объект, например класс с __call__ — удобно, когда нужно состояние:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def ping():
    return "pong"

ping(); ping()
print(ping.calls, ping.__name__)

Декоратор класса

Декоратор можно применить и к классу — он получает класс и возвращает (обычно тот же) класс. Так устроен @dataclass:

def add_repr(cls):
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({fields})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class User:
    def __init__(self, name, age):
        self.name, self.age = name, age

print(User("Анна", 28))

singledispatch — перегрузка по типу

from functools import singledispatch

@singledispatch
def describe(x):
    return f"что-то: {x!r}"

@describe.register
def _(x: int):
    return f"целое число {x}"

@describe.register
def _(x: list):
    return f"список из {len(x)} элементов"

print(describe(5), describe([1, 2]), describe("s"), sep="\n")

Итог

  • Декоратор с параметрами — фабрика из трёх уровней: params → decorator(func) → wrapper(*args, **kwargs).
  • func=None, *, ... позволяет поддержать @deco и @deco(...).
  • lru_cache/cache — мемоизация чистых функций с хешируемыми аргументами; cache_info().
  • Декоратором может быть класс с __call__; декорировать можно и классы.
  • singledispatch выбирает реализацию по типу первого аргумента.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Напишите декоратор с параметром repeat(times), который вызывает функцию times раз и возвращает список результатов.

Какие тесты проверяют код (2)
@repeat(3)
def hi(name):
    return f"hi {name}"
assert hi("Аня") == ["hi Аня", "hi Аня", "hi Аня"]

@repeat(times=2)
def one():
    return 1
assert one() == [1, 1] and one.__name__ == "one"

Задание 2 Программа на Python

Ускорьте рекурсивную функцию fib(n) с помощью functools.lru_cache (или cache), чтобы fib(200) считалось мгновенно.

Какие тесты проверяют код (3)
assert fib(10) == 55
assert hasattr(fib, "cache_info"), "Используйте lru_cache или cache"
assert hasattr(fib, "cache_info") and fib(200) == 280571172992510140037611932413038677189525
Задание 3 Программа на Python

Напишите декоратор validate_types, который проверяет типы аргументов по аннотациям функции (только позиционные аргументы) и при несовпадении бросает TypeError. Подсказка: func.__annotations__ — словарь «имя параметра → тип», имена параметров по порядку — inspect.signature(func).parameters.

Какие тесты проверяют код (1)
@validate_types
def area(w: int, h: int):
    return w * h
assert area(2, 3) == 6
try:
    area(2, "3")
    ok = False
except TypeError:
    ok = True
assert ok, "area(2, \"3\") должна бросать TypeError"

Задание 4 Один вариант ответа

Сколько уровней вложенных функций у декоратора с параметрами @retry(times=3)?

Задание 5 Несколько вариантов

Что верно про functools.lru_cache?

Войдите, чтобы отмечать пройденные уроки.