Как и обещал, приведу список полезных декораторов. Среди них как стандартные поставляемые вместе с Python, так и декораторы из других библиотек и исходные коды прочих интересных декораторов.
Начнем с самых известных.
Свойства @property
Декоратор @property
облегчает создание свойств в классах Python. Свойства выглядят как обычные атрибуты (поля) класса, но при их чтении вызывается геттер (getter), при записи – сеттер (setter), а при удалении – делитер (deleter). Геттер и делитер опциональны.
Краткая справка. Свойства нужны, чтобы элегантно в стиле ООП обрабатывать работу с полями класса. В ООП с полями класса не работают напрямую, они сокрыты. В интерфейс взаимодействия выводятся геттеры и сеттеры. Кроме того, геттер может производить вычисления, если свойство не хранится в классе, а является результатом какой-то математической формулы. А сеттер может проверять входные данные на корректность и вызывать побочные эффекты, например, сеттер при установке позиции картинки на экране может вызвать перерисовку экрана.
Декоратор @property
возвращает объект-дескриптор. О них я еще не рассказывал, но обязательно расскажу. @property
встроен и виден без import.
Примеры. Вес коробки не может быть отрицательным:
class Box: def __init__(self): self.__weight = 0 @property def weight(self): return self.__weight @weight.setter def weight(self, new_weight): if new_weight < 0: raise ValueError('negative weight') self.__weight = new_weight b = Box() b.weight = 100 print(b.weight) # 100 b.weight = -10 # ValueError
Вычислимое свойство:
class Circle: def __init__(self, r): self.r = r @property def area(self): return 3.1415 * self.r**2 c = Circle(10) print(c.area)
Статические и классовые методы
Методы могут быть не только у экземпляра класса, но и у самого класса, которые вызываются без какого-то экземпляра (без self). Декораторы @staticmethod
и @classmethod
как раз делают метод таким (статическим или классовым). Эти декораторы встроены и видны без import.
Статический метод – это способ поместить функцию в класс, если она логически относится к этому классу. Статический метод ничего не знает о классе, из которого его вызвали.
class Foo: @staticmethod def help(): print('help for Foo class') Foo.help()
Классовый метод напротив знает, из какого класса его вызывают. Он принимает неявный первый аргумент (обычно его зовут cls
), который содержит вызывающий класс. Классовые методы прекрасно подходят, когда нужно учесть иерархию наследования. Пример: метод group
создает список из нескольких людей. Причем для Person – список Person, а для Worker – список Worker. Со @staticmethod такое бы не вышло:
class Person: @classmethod def group(cls, n): # cls именно тот класс, который вызвал return [cls() for _ in range(n)] def __repr__(self): return 'Person' class Worker(Person): def __repr__(self): return 'Worker' print(Person.group(3)) # [Person, Person, Person] print(Worker.group(2)) # [Worker, Worker]
@contextmanager
Этот декоратор позволяет получить из генератора – контекст менеджер. Находится в стандартном модуле contextlib. Пример открытие файла.
from contextlib import contextmanager @contextmanager def my_open(name, mode='r'): # тут код для получения ресурса f = open(name, mode) print('Файл открыт:', name) try: yield f finally: # Code to release resource, e.g.: f.close() print('Файл закрыт:', name) # использование with my_open('1.txt', 'w') as f: f.write('Hello') f.fooooo() # <- error # Файл открыт: 1.txt # Traceback (most recent call last): # Файл закрыт: 1.txt
В этом генераторе есть единственный yield
– он возвращает как раз нужный ресурс. Все, что до него – код захвата ресурса (будет выполнен в методе __enter__
), например, открытие файла. Мы оборачиваем yield
в try/finally,
чтобы если даже в блоке кода with
произойдет ошибка, то исключение выбросится из yield
, но код закрытия файла в блоке finally
будет выполнен в любом случае. Код закрытия выполняется в методе __exit__
менеджера контекста.
Асинхронная версия этого декоратора – @asynccontextmanager
. Пример:
from contextlib import asynccontextmanager @asynccontextmanager async def get_connection(): conn = await acquire_db_connection() try: yield conn finally: await release_db_connection(conn) # использование async def get_all_users(): async with get_connection() as conn: return conn.query('SELECT ...')
Кэширование @lru_cache
Писал здесь подробно. Запоминает результаты функции для данного набора аргументов, при следующем вызове уже не выполняет функцию, а достает результат из кэша. Размер кэша регулируется. Часто используемые элементы остаются в кэше, редкие – вытесняются, если размер доходит до максимального.
Пример. Без кэша это рекурсивная функция чисел Фибоначчи была бы крайне неэффективна:
import functools @functools.lru_cache(maxsize=128) def fibonacci(n): if n == 0: return 0 elif n == 1: return 1 return fibonacci(n - 1) + fibonacci(n - 2)
@functools.wraps
Декоратор @functools.wraps полезен при разработке других декораторов. Передает имя, документацию и прочую мета-информацию из декорируемой функции к ее обертке. Подробнее в статье про декораторы.
@atexit.register
Декоратор @atexit.register регистрирует функцию для вызова ее при завершении работы процесса Python.
import atexit @atexit.register def goodbye(): print("You are now leaving the Python sector.")
Измерение времени @timeit
Переходим к самописным декораторам.
Этот декоратор измеряет время выполнения функции, которую декорирует.
import time from functools import wraps def timeit(method): @wraps(method) def timed(*args, **kw): ts = time.monotonic() result = method(*args, **kw) te = time.monotonic() ms = (te - ts) * 1000 all_args = ', '.join(tuple(f'{a!r}' for a in args) + tuple(f'{k}={v!r}' for k, v in kw.items())) print(f'{method.__name__}({all_args}): {ms:2.2f} ms') return result return timed # использование: @timeit def slow_func(x, y, sleep): time.sleep(sleep) return x + y slow_func(10, 20, sleep=2) # печатает: slow_func(10, 20, sleep=2): 2004.65 ms
Как видите, нам не нужно вмешиваться в код функции, не нужно каждый раз писать измеритель времени, декоратор отлично экономит нашу работу: надо измерить время – дописали @timeit
и видим все, как на ладони.
Кэшируемое свойство класса
Свойство класса, которое реально вычисляется один раз, а потом запоминается на ttl секунд:
import time class CachedProperty: def __init__(self, ttl=300): self.ttl = ttl def __call__(self, fget, doc=None): self.fget = fget self.__doc__ = doc or fget.__doc__ self.__name__ = fget.__name__ self.__module__ = fget.__module__ return self def __get__(self, inst, owner): now = time.monotonic() try: value, last_update = inst._cache[self.__name__] if self.ttl > 0 and now - last_update > self.ttl: raise AttributeError except (KeyError, AttributeError): value = self.fget(inst) try: cache = inst._cache except AttributeError: cache = inst._cache = {} cache[self.__name__] = (value, now) return value cached_property = CachedProperty # применение class Foo: @cached_property() def some_long_running_property(self): time.sleep(1) return 42 f = Foo() for _ in range(10): print(f.some_long_running_property)
Из 10 повторений только первое вызовет код геттера, а остальные возьмут значение из кэша.
Устаревший метод с @deprecated
При вызове метода или функции, помеченной декоратором @deprecated
будет выдано предупреждение, что этот метод или функция является устаревшими:
import logging from functools import wraps def deprecated(func): @wraps(func) def new_func(*args, **kwargs): logging.warning("Call to deprecated function {}.".format(func.__name__)) return func(*args, **kwargs) return new_func # примеры: @deprecated def some_old_function(x, y): return x + y class SomeClass: @deprecated def some_old_method(self, x, y): return x + y some_old_function(10, 20) # WARNING:root:Call to deprecated function some_old_function. SomeClass().some_old_method(20, 10) # WARNING:root:Call to deprecated function some_old_method.
Повторитель
Повторяет вызов функции n раз, возвращает последний результат.
from functools import wraps def repeat(_func=None, *, num_times=2): def decorator_repeat(func): @wraps(func) def wrapper_repeat(*args, **kwargs): value = None for _ in range(num_times): value = func(*args, **kwargs) return value return wrapper_repeat if _func is None: return decorator_repeat else: return decorator_repeat(_func) @repeat(num_times=5) def foo(): print('текст') foo() # текст # текст # текст # текст # текст
Замедлитель
Замедляет исполнение функции на нужное число секунд. Бывает полезно для отладки.
from functools import wraps import time def slow_down(seconds=1): def _slow_down(func): """Sleep 1 second before calling the function""" @wraps(func) def wrapper_slow_down(*args, **kwargs): time.sleep(seconds) return func(*args, **kwargs) return wrapper_slow_down return _slow_down @slow_down(seconds=0.5) def foo(): print('foo') def bar(): foo() # каждый foo по полсекунды foo() bar()
Помощник для отладки
Этот декоратор будет логгировать все вызовы функции и печатать ее аргументы и возвращаемое значение.
from functools import wraps # Печатает сигнатуру вызова и возвращаемое значение import functools def debug(func): @wraps(func) def wrapper_debug(*args, **kwargs): args_repr = [repr(a) for a in args] kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] signature = ", ".join(args_repr + kwargs_repr) print(f"Calling {func.__name__}({signature})") value = func(*args, **kwargs) print(f"{func.__name__!r} returned {value!r}") return value return wrapper_debug @debug def testee(x, y): print(x + y) testee(2, y=5) # Calling testee(2, y=5) # 7 # 'testee' returned None
Декораторы в Django и Flask
Декораторы активно задействованы в Django. Например, они позволяют налагать условия на обработку запросов или дополнять запрос дополнительной логикой:
@require_POST @login_required @transaction_atomic def some_view(request): ...
Пример с проверкой наличия зарегистрированного юзера для Flask:
from flask import Flask, g, request, redirect, url_for import functools app = Flask(__name__) def login_required(func): """Make sure user is logged in before proceeding""" @functools.wraps(func) def wrapper_login_required(*args, **kwargs): # если нет юзера, то редикректим на страницу логина if g.user is None: return redirect(url_for("login", next=request.url)) return func(*args, **kwargs) return wrapper_login_required @app.route("/secret") @login_required def secret(): ...
Еще хочу!
Вот ссылка на широкий набор декораторов и библиотек, их применяющих (на английском). Или вот тут.
🐉 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈