Топ-10 декораторов Python

Декораторы в Python — это нечто потрясающее! Они очень полезны в различных сценариях и невероятно просты в использовании.

В этой статье мы рассмотрим десятку невероятных декораторов, которые вы сможете использовать в вашем коде Python. Многие из них изменяют парадигму Python и действительно могут помочь вывести ваш код на новый уровень!

От редакции Pythonist. О том, что собой представляют декораторы, можно почитать в статье «Декораторы в Python: зачем они нужны и как их создавать».

Множественная диспетчеризация: dispatch

Прежде чем мы перейдем к тому, как использовать данный декоратор, давайте сначала обсудим, что такое множественная диспетчеризация.

Множественная диспетчеризация – механизм в языках программирования, позволяющий выбрать одну из нескольких функций в зависимости от динамических типов или значений аргументов.

Обычно при объектно-ориентированном подходе все функции Python должны быть определены в области видимости класса. Это, собственно, определение объектно-ориентированного программирования. Но Python не заставляет человека использовать эту парадигму, что является одним из достоинств этого языка. Большая часть кода Data Science в экосистеме Python очень методизирована и куда меньше фокусируется на типах, чем можно было бы ожидать от объектно-ориентированного языка.

Полиморфизм, реализованный при помощи множественной диспетчеризации, может вывести эту методологию на новый уровень. Это применимо даже внутри класса, как мы увидим в примере ниже.

Декоратор dispatch стоит использовать, потому что этот простой вызов может существенно изменить код и создать довольно хороший синтаксис.

Рассмотрим пример:

from multipledispatch import dispatch
class Pasta:
    def __init__(self, width, height, classification):
        self.width, self.height = width, height
        self.classification = classification
    def classify():
        if self.width > 3 and height < 2:
            self.classification = "Linguine"
        elif self.width == self.height and self.width < 3:
            self.classification = "Spaghetti"
        elif width > 2 and height == 1:
            self.classification = "Fettuccine"
        else:
            self.classification = "Dough"
class Bread:
    def __init__(self, width, height, classification):
        self.width, self.height = width, height
        self.classification = classification
    def classify():
        if self.width > 3 and height < 2:
            self.classification = "Pizza Crust"
        elif self.width == self.height and self.width < 3:
            self.classification = "White bread"
        elif width > 2 and height == 1:
            self.classification = "Flatbread"
        else:
            self.classification = "Dough"
class Roller:
    def __init__(self):
        pass
    def roll_thinner(x : Pasta):
        x.height -= 1
        x.width += 1
        x.classify()
    def cut(x : Pasta):
        x.width -= 1
        x.classify()
    def fold(x : Pasta):
        x.width += 1
        x.classify()

Здесь у нас есть 3 класса.

Первый объект – это Pasta. Это просто макароны, которые можно раскатывать с помощью нашей скалки (Roller).

Следующий объект — Bread (хлеб). Он имеет многие из атрибутов макаронных изделий, т.е. Pasta. Но есть одна вещь, которой не хватает в классе Bread. Проблема в том, что если мы включим это значение, то ни одна из функций Roller не будет работать с классом Bread! Обычно это означает, что нужны либо новые функции, работающие исключительно с этим типом, например, roll_thinnerbread() и roll_thinnerpasta() соответственно, либо второй класс Roller, который мы используем исключительно для Bread. К примеру, такой класс:

class BreadRoller:

Решение с помощью множественной диспетчеризации

Однако с помощью множественной диспетчеризации эти проблемы можно решить проще. Благодаря ей мы можем обрабатывать типы по-разному от функции к функции. В этом примере мы изменим только одно имя функции в классе Bread: classify() станет bake().

[python_ad_block]

Следующая функция выдаст нам новый объект с некоторыми предустановленными настройками dough (англ. «тесто»):

def make_dough(pasta = False):
    if pasta:
        return(Pasta(10, 10, "Dough"))
    else:
        return(Bread(10, 10, "Dough"))

Давайте-ка замесим тесто!

dough = make_dough()
print(dough.classification)
'Dough'

А теперь запустим функцию make_dough() для pasta:

pasta_dough = make_dough(pasta = True)

Теперь давайте «раскатаем» нашу пасту в красивые лингвини — Linguine — которые будем подавать вместе с только что созданным тестом для пиццы.

rollingpin = Roller()
while pasta_dough.height >= 3:
    rollingpin.roll_thinner(pasta_dough)
while pasta_dough.width > 2:
    rollingpin.cut(pasta_dough)
print(pasta_dough.classification)
Linguine

Здорово! Мы сделали Linguine! Но все это разваливается, потому что наш другой тип имеет немного другую структуру:

rollingpin.roll_thinner(dough)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-132-b755ead4a785> in <module>
----> 1 rollingpin.roll_thinner(dough)

<ipython-input-118-1af4dc2d94f2> in roll_thinner(self, x)
      5         x.height -= 1
      6         x.width += 1
----> 7         x.classify()
      8     def cut(self, x : Pasta):
      9         x.width -= 1

AttributeError: 'Bread' object has no attribute 'classify'

О, нет!

К счастью, теперь мы можем продемонстрировать ту самую множественную диспетчеризацию, изменив функции в нашем классе Roller, чтобы они принимали оба типа. Мы сделаем это путем простого клонирования функций. Изменим то, что нужно изменить, а затем с помощью декоратора dispatch сделаем так, чтобы этот код выполнялся только при передаче правильного типа.

class Roller:
    def __init__(self):
        pass
    @dispatch (Pasta)
    def roll_thinner(self, x : Pasta):
        x.height -= 1
        x.width += 1
        x.classify()
    @dispatch (Pasta)
    def cut(self, x : Pasta):
        x.width -= 1
        x.classify()
    @dispatch (Pasta)
    def fold(self, x : Pasta):
        x.width += 1
        x.classify()
    @dispatch (Bread)
    def roll_thinner(self, x : Bread):
        x.height -= 1
        x.width += 1
        x.bake()
    @dispatch (Bread)
    def cut(self, x : Bread):
        x.width -= 1
        x.bake()
    @dispatch (Bread)
    def fold(self, x : Bread):
        x.width += 1
        x.bake()
rollingpin.roll_thinner(dough)

Теперь этот код выполняется. Излишне говорить, что это может быть довольно удобно для многих сценариев! Множественная диспетчеризация – очевидный и очень удобный способ программирования. Он может быть очень гибким и создавать отличный синтаксис. И работать с ним легко!

Celery

Celery незаменим при работе с задачами в Python. Это, по сути, диспетчер задач для Python. Он может отслеживать очереди задач, которые должны выполняться рядом компьютеров или потоков. Слово «очередь» в компьютерном мире всегда связано с принципом FIFO (First in – first out) «первым пришел – первым ушел».

Единственная проблема с Celery в том, что его работу будет особенно сложно продемонстрировать. Для этого нам нужно было бы иметь в своем распоряжении рабочий сервер, с которым библиотека могла бы работать. Конечно, у нас этого нет. Вот базовый пример создания задач с помощью Celery из документации:

from celery import Celery

app = Celery('tasks', broker='pyamqp://guest@localhost//')

@app.task
def add(x, y):
    return x + y

Излишне говорить, что Celery невероятно прост в использовании. Просто создайте класс Celery, а затем используйте декораторы, чтобы поставить свои задачи в очередь. Доступные серверы от брокера затем возьмут на себя задачи.

Click

Click – еще один потрясающий модуль. Он помогает разработчикам Python создавать интерфейсы командной строки или CLI. Самое замечательное в нем то, что его можно использовать для предоставления аргументов командной строки Python. Ниже приведен пример, который будет приветствовать кого-то после синтаксического анализа его имени как CLI:

import click
@click.command()
@click.option("--name", prompt="Your name", help="The person to greet.")
def hello(count, name):
    click.echo(f"Hello, {name}!")
if __name__ == '__main__':
    hello()

Click – это довольно круто, потому что во многих отношениях он работает как код bash вместе с вашим Python. Но код будет работать не только с Unix-подобными операционными системами, и это еще одно достоинство Click.

deprecated

Устаревание кода влечет за собой проблемы. Вы хотите избавиться от функции, но кто-то все еще пытается ее использовать. Обычно вы пишете немного кода, чтобы создать предупреждение об устаревании. Для этого мы бы использовали модуль warn:

warnings.warn(
            "this function is deprecated... etc.",
            DeprecationWarning
        )

Однако, используя deprecated, нам нужно сделать всего лишь один простой вызов. Мы вызываем deprecated через нашу функцию как декоратор, и сразу же могут выдаваться предупреждения об устаревании.

from deprecated import deprecated
@deprecated ("This function is deprecated, please do not make dough here")
def make_dough(pasta = False):
    if pasta:
        return(Pasta(10, 10, "Dough"))
    else:
        return(Bread(10, 10, "Dough"))

При попытке запустить эту функцию мы получим предупреждение об устаревании:

z = make_dough()

Теперь каждый, кто попытается использовать эту функцию, будет уведомлен, что функция устарела и не нужно её использовать.

deco: concurrent

Deco – это модуль параллельных вычислений для Python. Deco очень похож на другой модуль, под названием Numba, но допускает синхронные и параллельные процессы.

Здесь мы рассмотрим декоратор concurrent.

Параметр concurrent использует multiprocessing.pool для одновременного выполнения операций. Затем это используется с другим декоратором для запуска функции внутри потока в фоновом режиме, продолжая заботиться о функции. Давайте рассмотрим это на примере с макаронами и скалкой (Roller):

from deco import concurrent, synchronized
def roll_linguine(count : int):
    rollingpin = Roller()
    pastas = []
    for i in range(0, count):
        dough = make_dough(pasta = True)
        while dough.height >= 3:
            rollingpin.roll_thinner(dough)
        while dough.width > 2:
            rollingpin.cut(dough)
        pastas.append(dough)
    return(pastas)

Кроме того, мы сделали аналог для пиццы:

def roll_pizzas(count : int):
    rollingpin = Roller()
    pizzas = []
    for i in range(0, count):
        dough = make_dough()
        while dough.height > 3:
            rollingpin.roll_thinner(dough)
        while dough.width > 2:
            rollingpin.cut(dough)

Чтобы сделать эти функции параллельными, мы просто добавляем декоратор:

@concurrent
def roll_linguine(count : int):
    rollingpin = Roller()
    pastas = []
    for i in range(0, count):
        dough = make_dough(pasta = True)
        while dough.height >= 3:
            rollingpin.roll_thinner(dough)
        while dough.width > 2:
            rollingpin.cut(dough)
        pastas.append(dough)
    return(pastas)
@concurrent
def roll_pizzas(count : int):
    rollingpin = Roller()
    pizzas = []
    for i in range(0, count):
        dough = make_dough()
        while dough.height > 3:
            rollingpin.roll_thinner(dough)
        while dough.width > 2:
            rollingpin.cut(dough)

deco: synchronized

Похоже, раньше мы допустили ошибку в наших попытках создания Pizza и Linguine… К сожалению, сегодня вечером у нас будет более 500 гостей! Нам нужно будет приготовить много Pizza и Linguine, если мы хотим, чтобы каждый гость был накормлен. Каждому гостю нужно 2 пиццы и не менее 20 Linguine. К счастью, поскольку две функции, которые мы использовали, работают одновременно, мы можем синхронизировать их с помощью декоратора synchronized от deco!

# linguine e pizza? molta bellisima!
@synchronized
def prepare_meal(count : int):
    roll_linguine(count * 200)
    roll_pizzas(count * 2)
%timeit prepare_meal(500)

Хорошая новость заключается в том, что все это работает, хотя и с нашим предупреждением об устаревании, сделанным ранее.

cachetools

Инструменты кеширования – еще один отличный пакет для повышения производительности Python. Подобные пакеты великолепны, потому что они действительно улучшают работу с Python.

Хорошо написанный код Python может быть довольно быстрым. Но не стоит забывать, что это язык сценариев и, следовательно, нам нужно стремиться к максимальной производительности кода. Кроме того, наш код не всегда безупречен! Имея это в виду, давайте рассмотрим еще один декоратор, который повысит производительность нашего пакета Python!

Декоратор называется cashed. Он может принимать множество различных аргументов для изменения типа используемого кеша и размера кешей.

Все это также пригодится для нашего небольшого бизнеса по производству Linguine и Pizza. Эту функцию можно использовать для хранения предыдущих расчетов для будущих вызовов. Это означает, что теперь наши Roller для пиццы действительно хорошо раскатывают пиццу, но они все равно устают после того, как раскатают 100 штук или другое установленное количество.

from cachetools import cached, LRUCache, TTLCache

Мы можем предоставить именованный аргумент, чтобы определить, какой кеш использовать. Следует отметить, что все эти кеши ведут себя по-разному.

Если вы используете этот код для большого приложения, в котором их, вероятно, будет много, но не хотите, чтобы ваш кеш перезаписывался, пока вы работаете с ним, можно использовать TTLCache. Этот кеш исчезает через 10 минут, но не перезаписывается.

С другой стороны, LRUCache может быть перезаписан, но при этом может работать вечно – это лучший выбор для общих случаев использования. Есть также версия, которая не имеет аргументов – cached.

Tomorrow

Tomorrow – еще одна библиотека, которую можно использовать для повышения производительности языка программирования Python. Декоратор для этого называется threads. Его можно вызвать с позиционным аргументом, чтобы указать, сколько потоков мы хотели бы выделить для задачи, а затем можно установить именованные аргументы – например, timeout.

from tomorrow import threads
@threads(5)
def prepare_meal(count : int):
    roll_linguine(count)
    roll_pizzas(count)

А вот пример использования именованного аргумента:

@threads(5, timeout = 1)
def prepare_meal(count : int):
    roll_linguine(count)
    roll_pizzas(count)

Tenacity

Tenacity – более уникальный пакет, который можно использовать, чтобы попробовать еще раз в случае, если какой-то код не выполняется должным образом. Его можно использовать с ПО, находящимся в стадии разработки, или, возможно, с запросами, если они не выполняются. У такой вещи есть сотни применений, а поскольку она заключена в декоратор, ее можно использовать невероятно легко!

С учетом природы этого пакета, единственный способ демонстрации его работы, который пришел на ум, это выбросить print:

from tenacity import retry
@retry
def ohno():
    print("function ran once")
    try:
        2 == 3 - 5 == 6
    except ValueError:
        print("Idk what that was even meant to do")
ohno()

Всякий раз, когда мы запускаем код, он выполняется один раз, а затем «крашит» ядро операционной системы. Так что, как говорится, не повторяйте в домашних условиях!

property

Последний декоратор, который мы рассмотрим, будет декоратором свойств property. Он из стандартной библиотеки, а часть декоратора используется для превращения метода в «геттер» для одноименного атрибута только для чтения. Другими словами, мы просто делаем этот атрибут копией предыдущего атрибута, доступной только для чтения.

class PowerLine:
    def __init__(self):
        self._voltage = 100000
    @property
    def voltage(self):
        return self._voltage

Заключение

Декораторы – отличная функция языка программирования Python. Они особенно хороши тем, что их можно использовать для изменения способа взаимодействия типа с окружающими его методами. Еще одна замечательная вещь в декораторах – это то, что они часто могут ускорять Python.

Надеемся, вооружившись списком декораторов из этой статьи , вы сможете ускорить работу ваших функций Python! Спасибо большое, что прочитали данную статью!

Успехов в написании кода!

Перевод статьи «10 Of My Favorite Python Decorators».