Полезные декораторы

Как и обещал, приведу список полезных декораторов. Среди них как стандартные поставляемые вместе с 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 👈 

Добавить комментарий