Декораторы с параметрами, wraps и lru_cache
Декоратор с параметрами
@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())
Декоратор с параметрами и без
Иногда хочется писать и @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
Напишите декоратор с параметром 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"
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return [func(*args, **kwargs) for _ in range(times)]
return wrapper
return decorator
Ускорьте рекурсивную функцию 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
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
Напишите декоратор 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"
import functools
import inspect
def validate_types(func):
names = list(inspect.signature(func).parameters)
hints = func.__annotations__
@functools.wraps(func)
def wrapper(*args, **kwargs):
for name, value in zip(names, args):
expected = hints.get(name)
if expected is not None and not isinstance(value, expected):
raise TypeError(f"{name}: ожидался {expected.__name__}, получен {type(value).__name__}")
return func(*args, **kwargs)
return wrapper
Сколько уровней вложенных функций у декоратора с параметрами @retry(times=3)?
Что верно про functools.lru_cache?
Войдите, чтобы отмечать пройденные уроки.