Декораторы, как средство для самосовершенствования кода в Python Python

Декораторы, как средство для самосовершенствования кода в Python

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

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

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

Проще всего способность к самосовершенствованию для наших функций и классов можно было реализовать на основе обеспечения их взаимодействия с другими (корректирующими) функциями и классами от сторонних разработчиков. Такое взаимодействие с одной стороны должно через аргументы в базовых функциях или классах (программных объектах) внедрять в них дополнительные корректирующие программные объекты, а с другой стороны возвращать из этих базовых функций и классов их модифицированные аналоги.

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

Два способа вызова функций и классов

Любая функция или класс (программный объект) в Python могут быть вызваны либо с круглыми скобками, либо без них. Вызов программного объекта со скобками запускает его на выполнение. Вызов этого объекта без скобок, либо указывает на место его размещения в ОЗУ нашего вычислительного средства (в случае если он уже инициализирован), либо на его принадлежность к функции или классу (в случае, если этот объект еще не инициализирован). Эти два варианта возможного вызова присущи, как нашим самостоятельно созданным программным объектам, так и объектам, импортированным из стандартных модулей Python. Давайте посмотрим, как это выглядит на практике:

# ЧАСТЬ I
# Определяем простую пользовательскую функцию
def function1(name):
    print("Привет", name)

# Запускаем эту функцию на выполнение
function1("Вася")
# Привет Вася

# Выводим эту же функцию, как объект в памяти нашего компьютера
print(function1)
# <function function1 at 0x000001FA06E9FEC0>

# ЧАСТЬ II
# Формируем список и выводим его, преобразованным в кортежи за счет
# класса enumerate
film_genre = ["Боевик", "Вестерн", "Детектив", "Драма", "Комедия"]
print(list(enumerate(film_genre)))
# [(0, 'Боевик'), (1, 'Вестерн'), (2, 'Детектив'), (3, 'Драма'), (4, 'Комедия')]

# Выводим содержимое национализированного класса enumerate
print(enumerate)
<class 'enumerate'>

# Выводим экземпляр класса enumerate, инициализированный списком film_genre
print(enumerate(film_genre))
# <enumerate object at 0x000001FA07448CC0>

Из листинга во 2-й части нашего примера мы внезапно увидели, что популярная и хорошо знакомая нам функция enumerate из стандартной библиотеке Python, вдруг оказалась классом. Это, пожалуй, и все то полезное, что мы подчерпнули от вызова функций и классов без скобок. Но, дело в том, что возможность вызова программных объектов без скобок в Python предусмотрена отнюдь не столько для их взаимодействия с разработчиками, сколько для осуществления передачи этих объектов любим другим функциям и классам в качестве аргументов. Мы более подробно рассмотрим такую передачу функций и классов в качестве аргументов далее в этой статье, а сейчас давайте пока разберемся чем же все-таки отличаются функции и классы от других объектов в Python, почему они могут вызываться не одним, а двумя способами и, есть ли вообще в Python иные объекты, наделенные аналогичной возможностью.

Метод __call__ и его основное предназначение

Любой объект в Python содержащий метод __call__ может быть вызван с круглыми скобками, что позволит ему запустить этот самый метод __call__ на выполнение. Давайте посмотрим, как это работает на простеньком примере в рамках отбора претендентов на участие в конкурсе по предоставлению индивидуальных грантов:

# Определим простенький класс Grazhdanstvo_US
class Grazhdanstvo_US:
    def __init__(self, name):
        self.name = name

# Создадим на основе этого класса его экземпляр konkursant_vasya,
# который попробуем вызвать с круглыми скобками
konkursant_vasya = Grazhdanstvo_US("не имеет")
konkursant_vasya()
# TypeError: 'Grazhdanstvo_US' object is not callable

# Добавим в класс Grazhdanstvo_US метод __call__
class Grazhdanstvo_US:
    def __init__(self, name):
        self.name = name
    def __call__(self):
        print("Гражданство США -", self.name)

# и снова создадим из этого класса экземпляр konkursant_vasya,
# который попробуем вызвать с круглыми скобками
konkursant_vasya = Grazhdanstvo_US("не имеет")
konkursant_vasya()
# Гражданство США - не имеет

В случае с классом Grazhdanstvo_US без метода __call__, вызов его экземпляра с круглыми скобками прогнозировано вызывает ошибку, свидетельствующую о том, что объекты (экземпляры) данного класса не являются вызываемыми. Если же в наш класс Grazhdanstvo_US мы добавляем метод __call__, то все созданные из данного класса экземпляры безукоризненно вызываются, выполняя при этом данный унаследованный ими метод.

На самом деле, вышеназванный метод __call__ есть абсолютно у всех пользовательских и библиотечных программных объектах Python. Давайте убедимся в этом на нижеследующем примере:

# Определяем простую пользовательскую функцию и смотрим есть ли у нее
# метод __call__
def function1(name):
    print("Привет", name)
print(function1.__call__)
# <method-wrapper '__call__' of function object at 0x000001B228ECFEC0>

# Смотрим, есть ли метод __call__ у нашей любимой функции print() из
# стандартной библиотеки Python
print(print.__call__)
# <method-wrapper '__call__' of builtin_function_or_method object at
# 0x000001B228E75490>

# Смотрим, есть ли метод __call__ у хорошо известной нам функции enumerate(),
# которая, как мы выяснили раннее, на самом деле оказалось классом из стандартной
# библиотеки Python
print(enumerate.__call__)
# <method-wrapper '__call__' of type object at 0x00007FFCF357DD90>

Передача функций и классов в качестве аргументов другим программным объектам

Теперь настало время разобраться, как конкретно мы можем использовать наши пользовательские функции и классы (программные объекты) для их передачи объектам из арсенала библиотек Python или же с целью приема в них внешних программных объектов из тех же вышеназванных библиотек. А начнем мы с рассмотрения использования стандартной Python функции sorted(), которая совместно с написанной нами пользовательской функцией по формированию ключа сортировки, должна будет отсортировать в алфавитном порядке следующий список жанров фильмов.

film_genre = ["боевик", "Трагедия", "детектив", "драма", "комедия",
              "Сказка",  "Нуар"]

Если мы попробуем отсортировать вышеприведенный список просто применив функцию sorted(), то получим абсолютно не устраивающий нас результат:

film_genre = ["боевик", "Трагедия", "детектив", "драма", "комедия",
              "Сказка",  "Нуар"]
print(sorted(film_genre))

# ['Нуар', 'Сказка', 'Трагедия', 'боевик', 'детектив', 'драма', 'комедия']

Дело в том, что составители данного списка (видимо специально для привлечения внимания) указали с большой буквы те жанры фильмов, которые пользуются у зрителей наименьшей популярностью. Они таки да, добились своего и, в результирующем списке после sorted(), эти забитые зрителями жанры стоять первыми. Но это отнюдь не укладывается в наши ожидания по сортировке списка именно в алфавитном порядке. Проблема здесь состоит в том, что функция sorted() без дополнительных аргументов сортирует наш список на основе ASCII/Unicode таблицы кодов, где сначала перечисляются все заглавные буквы, а затем уже - все строчные.

Вышеописанная проблема решается путем включения в вызов sorted() дополнительного поименованного аргумента key, который в качестве своего значения должен принимать пользовательскую функцию, принимающую, обрабатывающую и выдающую результат для каждого элемента сортируемого списка или иной структуры данных (в нашем случае, это список film_genre).

Давайте же попробуем сформировать нашу пользовательскую функцию для поименованного аргумента key , таким образом, чтобы она все элементы нашего списка film_genre переводила в нижней регистр:

def strochnyye_bukvy(element):
    return element.lower()

Как видим, наша функция создана для приема и обработки лишь одного значения. Но это требование относится сугубо для sorted(). На самом деле у каждой функции или класса, которые принимают какие-либо пользовательские программные объекты могут быть свои специфические требования к ним.

Для того, чтобы наглядно посмотреть, что у нас получилось, ниже приведем листинг всей программы, результат которой выведем через print() в переменной zhanry_po_alfavitu:

def strochnyye_bukvy(element):
    return element.lower()

film_genre = ["боевик", "Трагедия", "детектив", "драма", "комедия",
              "Сказка",  "Нуар"]
zhanry_po_alfavitu =  sorted(film_genre, key=strochnyye_bukvy)
print(zhanry_po_alfavitu)

# ['боевик', 'детектив', 'драма', 'комедия', 'Нуар', 'Сказка', 'Трагедия']

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

Теперь пришло время разобраться, как все-таки создавать собственные функции, классы или другие объекты, могущие вызываться с круглыми скобками, которые бы в качестве аргументов принимали бы иные себе подобные программные конструкции. Посмотрим, как это можно сделать на примере программки для моделирования вариантов выпадения сторон монетки (орел или решка), сторон кубика (от одного до шести) и мастей из 36 карт. Для наглядности, запишем сначала листинг всей этой программки, а затем разберем ее более подробно:

import random
monetka = ['Орел', 'Решка']
kubik = ['один', 'два', 'три', 'четыре', 'пять', 'шесть']
koloda_kart = ['Туз ♦', 'Король ♦', 'Дама ♦', 'Валет ♦', '10 ♦', '9 ♦',
               '8 ♦', '7 ♦', '6 ♦', 'Туз ♥', 'Король ♥', 'Дама ♥',
               'Валет ♥', '10 ♥', '9 ♥', '8 ♥', '7 ♥', '6 ♥', 'Туз ♣',
               'Король ♣', 'Дама ♣', 'Валет ♣', '10 ♣', '9 ♣', '8 ♣',
               '7 ♣',  '6 ♣', 'Туз ♠', 'Король ♠', 'Дама ♠', 'Валет ♠',
               '10 ♠', '9 ♠', '8 ♠', '7 ♠', '6 ♠']

def event_simulation(func, spisok, raz):
    kol_elementov = len(spisok)
    for j in range(raz):
        spisok1 = []
        for element in spisok:
            i = func(kol_elementov)
            spisok1.append(spisok[i])
        print(spisok1)

event_simulation(random.randrange, monetka, 3)
# ['Решка', 'Орел']
# ['Решка', 'Решка']
# ['Орел', 'Орел']

event_simulation(random.randrange, kubik, 2)
# ['четыре', 'четыре', 'три', 'шесть', 'три', 'один']
# ['два', 'шесть', 'один', 'пять', 'три', 'четыре']

event_simulation(random.randrange, koloda_kart, 1)
# ['8 ♠', '8 ♣', 'Дама ♦', '9 ♠', 'Туз ♦', 'Король ♥', 'Туз ♥', '7 ♠',
# '9 ♠', '9 ♣', '6 ♥', 'Туз ♦', '9 ♥', '8 ♣', '10 ♣', '8 ♦', '7 ♠',
# '10 ♦', '6 ♦', '6 ♥', 'Король ♥', '7 ♥', 'Туз ♠', 'Туз ♠', '10 ♣',
# '7 ♥', 'Валет ♥', '8 ♣', 'Король ♣', '7 ♣', '6 ♣', '9 ♣', '8 ♥',
# 'Валет ♦', '9 ♣', 'Король ♠']

В вышеприведенном коде определена пользовательская функция event_simulation(), которая принимает следующие три аргумента:

  • Внешнюю функцию, которой в нашем случае для всех трех примеров является функция random.randrange(), импортированная из стандартного Python модуля random и возвращающая случайное число в пределах от нуля до заданного числа (для нас таким заданным числом является количество элементов в принимаемом списке).
  • Список, на основе которого требуется смоделировать варианты выпадения его элементов.
  • Количество опытов, которое требуется провести с принимаемым списком.

Результатом нашей пользовательской функции является распечатка новой версии списка, предварительно принятого ею в качестве соответствующего аргумента. Данные версии списка формируются на основе случайных чисел и распечатываются столько раз, сколько было задано последним аргументом этой функции. Все это наглядно видно на листинге в виде распечатанных результатов после каждого из трех вызовов функции event_simulation() с различными аргументами.

Примечание. Так как мы имеем дело со случайными числами, то вполне очевидно, что приведенные на нашем листинге результаты выполнения функции event_simulation() будут разнится с результатами, полученными в вашем конкретном случае.

Важно отметить, что в качестве первого аргумента нашей пользовательской функции может свободно выступать и любая другая внешняя функция или класс. Главное, чтобы выводимый этой внешней функцией (классом) результат, согласовывался бы с внутренней логикой event_simulation(). Например, при условии предварительной обработки, в нашей пользовательской функции можно свободно использовать стандартные Python функции получения случайных чисел на основе использования тех или иных статистических распределений.

Главное, при создании пользовательских вызываемых объектов нам важно помнить, что хотя внешние функции и классы, принимаются в подобные объекты сугубо без скобок, внутри этих объектов все полученные в качестве аргументов внешние функции и классы всегда используются абсолютно обычным образом – только с круглыми скобками**.

Декораторы и их применение в Python

В предыдущем разделе статьи мы с вами рассмотрели возможность приема в наши пользовательские функции и классы внешних вызываемых объектов (тех же функций или классов) с целью изменения логики действия первых под влиянием тех или иных внешних факторов. Но, к сожалению, такой подход к уподоблению наших программных объектов объектам из реального мира практически не жизнеспособен. Ведь мы никогда не сможем предвидеть, какие факторы могут повлиять на корректность работы наших программ с течением времени и, заранее подготовиться к ним. Это все-равно, как попытаться полностью обезопасить человека от любых болезней, сделав ему все возможные прививки, от различных психологических травм, заранее проведя с ним соответствующие тренинги, от пожаров, наводнений, отравлений и прочих гадостей, которые могут с ним случится, заранее научив этого человека, как с этими всеми гадостями бороться.

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

Аналогичный подход, который называется декорированием применяют и в программировании на Python. Здесь под нашим «больным» может подразумеваться любой вызываемый объект (функция, класс и т.д.), который оборачивается в так называемый декоратор (внешнюю функцию, класс или иной вызываемый объект) с целью своей модификации и формирования на выходе себе подобного, но уже соответствующим образом измененного (адаптированного), объекта.

Посмотрим, как это может работать на примере пользовательской функции tip_chisla(), определяющей является ли введенный в нее аргумент тривиальным (простым) или составным числом:

def tip_chisla(chislo):
    for n in range(2, chislo//2):
        if chislo % n == 0:
            print(f'Аргументом этой функции выступает {chislo}, \
                  являющееся СОСТАВНЫМ числом')
            return False
        print(f'Аргументом этой функции выступает {chislo}, \
              являющееся ТРИВИАЛЬНЫМ числом')
        return True

Если мы зададим для этой функции аргумент с очень большим значением, то расчет и вывод окончательного результата у нее займет довольно продолжительное, заметное для нас время. Если же мы запустим эту функцию с тем же аргументом повторно, то время ее работы ничуть не уменьшится и будет таким же, как и в первый раз. В определенной степени, данное поведение нашей функции tip_chisla() несколько иррационально, поскольку в соответствии со здравым смыслом повторный вывод результата по уже предварительно обработанному аргументу должен быть моментальным.

Вышеназванный дефект нашей функции можно устранить (излечить) с помощью имеющегося в стандартном Python модуле functools декоратора lru_cache, который обеспечивает занесение себе в кэш-память результатов от выполнения обворачиваемых им функций или классов. При этом, если обернутый данным декоратором вызываемый объект повторно сталкивается с уже обработанными им данными, то его выдаваемый результат будет сформирован не за счет повторной обработки этих же данных, а за счет взятия уже имеющихся результатов, хранимых в кэш-памяти.

Посмотрим, как работает вышеописанный декоратор lru_cache на примере нашей пользовательской функции tip_chisla():

from functools import lru_cache
def tip_chisla(chislo):
    for n in range(2, chislo//2):
        if chislo % n == 0:
            print(f'Аргументом этой функции выступает {chislo}, \
                  являющееся СОСТАВНЫМ числом')
            return False
        print(f'Аргументом этой функции выступает {chislo}, \
              являющееся ТРИВИАЛЬНЫМ числом')
        return True

tip_chisla(73729259)
# Аргументом этой функции выступает 73729259, являющееся ТРИВИАЛЬНЫМ числом
tip_chisla(73729259)
# Аргументом этой функции выступает 73729259, являющееся ТРИВИАЛЬНЫМ числом

tip_chisla = lru_cache(tip_chisla)

tip_chisla(73729260)
# Аргументом этой функции выступает 73729260, являющееся СОСТАВНЫМ числом
tip_chisla(73729260)
# Аргументом этой функции выступает 73729260, являющееся СОСТАВНЫМ числом

В этом листинге вызов нашей функции tip_chisla() происходит аж 4-ре раза. При этом, после первых двух вызовов нашей функции с аргументом 73729259, для получения от нее результата нам приходится некоторое время ждать. Затем, мы оборачиваем функцию tip_chisla() декоратором lru_cache за счет следующего выражения:

tip_chisla = lru_cache(tip_chisla)

и опять запуская нашу функцию с немного измененным аргументом 73729260, немного ждем вывода результата. Когда же в четвертый раз мы запускаем нашу функцию с тем же аргументом, что и в третий раз, то в ответ получаем моментальный результат. Это происходит за счет того, что после оборачивания декоратором lru_cache наша функция несколько изменилась и, теперь может брать результаты для повторно обрабатываемых аргументов непосредственно из своей кэш-памяти.

На самом деле, оборачивание декораторами вызываемых объектов (функций или классов) на основе оператора присваивания, показанное в листинге из предыдущего примера, используется в кодинге крайне редко. Python предусматривает другой, гораздо более логически структурированный, компактный и визуально наглядный способ применения декораторов за счет использования символа @. Давайте перепишем и задекорируем нашу функцию из прошлого примера с использованием вышеназванного символа @:

from functools import lru_cache

@lru_cache
def tip_chisla(chislo):
    for n in range(2, chislo//2):
        if chislo % n == 0:
            print(f'Аргументом этой функции выступает {chislo}, \
                  являющееся СОСТАВНЫМ числом')
            return False
        print(f'Аргументом этой функции выступает {chislo}, \
              являющееся ТРИВИАЛЬНЫМ числом')
        return True

tip_chisla(73729259)
# Аргументом этой функции выступает 73729259, являющееся ТРИВИАЛЬНЫМ числом

В сущности, все что нам нужно для декорирования нашей функции tip_chisla(), как и любых других вызываемых объектов, так это просто перед их объявлением указать без пробелов знак @ и имя обворачивающего их декоратора. Под декораторами здесь понимаются определенным образом сформированные для декорирования, функции или классы, как в собственном исполнении, так и импортированные из стандартных модулей Python.

Основным преимуществом вышепоказанного способа декорирования является то, что он дает возможность декорировать вызываемые объекты (функции и классы) непосредственно в месте их создания (объявления) тем самым позволяя не только сокращать код, но и производить многократное оборачивание вашего кода декораторами различного предназначения.

Выше мы рассмотрели пример декорирования собственной пользовательской функции декоратором lru_cache из стандартного Python модуля functools. Теперь же давайте на практическом примере разберемся, как можно декорировать метод собственного класса, так чтобы при вызове его можно было бы использовать не как метод, а как свойство, но при этом автоматически обновлять его значение при изменении иных свойств нашего класса. Для решения этой задачи воспользуемся декоратором @property, являющимся классом из стандартной библиотеке Python:

class Kvadrat:
    def __init__(self, storona):
        self.storona =  storona
    @property
    def ploshchad(self):
        print(self.storona**2)
        return self.storona**2

kv = Kvadrat(4)

kv.ploshchad
# 16

kv.storona = 8

kv.ploshchad
# 64

print(kv)
# <__main__.Kvadrat1 object at 0x000001D5EDDA2350>

Как мы видим из примера выше, благодаря декоратору @property метод .ploshchad() в нашем классе Kvadrat теперь ведет себя, как свойство .ploshchad т.е. может вызываться без скобок, но при этом способно автоматически изменятся в соответствии с изменением стороны квадрата. На самом же деле, под капотом интерпретатора .ploshchad не является ни свойством, ни методом. В реальности, это тот экземпляр класса property, который получился в результате влияния на вышеназванный оригинальный метод декоратора @property. К счастью, подобные тонкости нас с вами должны интересовать в последнюю очередь. Но, все же следует быть в курсе того, что декораторы могут принимать и возвращать вызываемые объекты в самых разнообразных сочетаниях, которые могут быть продиктованы лишь многообразием реального мира.

Давайте к уже рассмотренному в предыдущем примере классу Kvadrat попробуем применить еще один декоратор @dataclass, который специально предусмотрен для упрощения работы со структурами данных и буквально превращает обычные классы в классы данных. Этот декоратор находится в Python модуле dataclasses, и применяется ко всему классу в целом. Ниже приведен пример того, как будет выглядеть наш класс, после применения к нему декоратора @dataclass:

from dataclasses import dataclass
@dataclass
class Kvadrat:
    storona: float
    @property
    def ploshchad(self):
        print(self.storona**2)
        return self.storona**2

kv = Kvadrat(4)

kv.ploshchad
# 16

print(kv)
# Kvadrat(storona=4)

Как видим, сама структура класса Kvadrat значительно упростилась вследствие того, что в нем уже отсутствует метод __init__(), который включается декоратором @dataclass в него уже автоматически. Вместе с тем, в этом классе четко прописан тип аргумента float, который требуя сопоставимости при инициализации экземпляров этого класса, значительно снижает вероятность ошибок при кодинге. Ну, и наконец, как вы могли заметить, при выводе экземпляра класса на печать через print(kv) в предыдущем примере выводится какая-то несуразица, тогда как в последнем примере четко отражается содержание этого экземпляра. И все эти «вкусняшки» появились только из-за того, что мы обернули наш класс декоратором @dataclass всего лишь добавив его перед определением данного класса.

Основы собственноручного создания декораторов

Любой декоратор образно можно представить, как процесс формирования некой среды, приспособленной для запуска в ней поглощаемого (оборачиваемого) объекта с целью получения от него релевантных результатов согласно заданным критериям. Одним из наиболее наглядных примеров данного процесса в реальном мире является сельскохозяйственное производство, которое можно представить в роли декоратора по отношению к брошенным в почву и взращенным впоследствии семенам, выступающим в качестве объекта, призванного в конечном итоге обеспечить нам как можно больший денежный доход. Для того, чтобы получить этот самый денежный доход, нам сначала нужно подготовить почву к посеву (запуску объекта) вспахав и удобрив ее. Затем непосредственно осуществить запуск объекта засеяв почву семенами, взрастив их и получив из них урожай. И, наконец нам следует переработать и продать полученный урожай с максимальной выгодой для нас.

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

1. Предварительное создание условий для запуска оборачиваемого объекта в нужном режиме 2. Непосредственный запуск оборачиваемого объекта 3. Переработка данных, полученных в результате запуска оборачиваемого объекта

Теперь давайте снова возвратимся к нашему примеру с сельскохозяйственным производством, и представим себе, чтобы было, если бы мы просто засеяли семена в неподготовленную почву или же сразу продали бы урожай без его предварительной переработки, например, в муку. В сущности, ничего страшного не случилось бы. Следовательно, любое из вышеописанных действий является необязательным и, целесообразно лишь при наличии от него оправданного экономического или иного эффекта. Точно также, 1-я и 3-я составляющие в нашем приведенном выше алгоритме по внутренней работе декораторов являются отнюдь необязательными и, могут быть опущены при написании соответствующего реального кода.

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

def dekorator_function(func):
    # Базовая функция для любого декоратора функций
    def wrapper(*args, **kwargs):
        # Предварительная обработка (1-я составляющая)
        print("Параметры вызова обертываемой функции -", args, kwargs)
        # Запуск оборачиваемой функции (2-я составляющая)
        rezultat_function = func(*args, **kwargs)
        # Обработка полученных результатов (3-я составляющая)
        print("Возвращаемое значение функции -", rezultat_function)
        return rezultat_function
    # Возврат обертываемой функции после ее модификации
    return wrapper

Код нашего декоратора начинается с объявления функции wrapper(), которая является базовой (обязательной) для декораторов функций и имеет такие поименованные аргументы, как *args и **kwargs. Аргумент *args предназначен для приема в wrapper() произвольного количества аргументов из оборачиваемой функции, а аргумент **kwargs - для приема дополнительных поименованных аргументов самого декоратора, которые могут указываться в скобках непосредственно после имени создаваемого декоратора. К сожалению, описания тонкостей работы с вышеназванными аргументами, как с самой функцией wrapper() находится вне целей этой статьи. Однако, вы всегда сможете подчерпнуть необходимую информацию по данной теме из многообразных источников в Internet.

Далее, каждая строка в вышеприведенном коде имеет свои комментарии и более-менее понятна.

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

def dekorator_function(func):
    def wrapper(*args, **kwargs):
        print("Параметры вызова обертываемой функции -", args, kwargs)
        rezultat_function = func(*args, **kwargs)
        print("Возвращаемое значение функции -", rezultat_function)
        return rezultat_function
    return wrapper

@dekorator_function
def function1(name):
    print("Привет", name)

function1('Вася')
# Параметры вызова обертываемой функции - ('Вася',) {}
# Привет Вася
# Возвращаемое значение функции - None

В конце данного листинга приведен результат выполнения нашей модифицированной функции function1(), который наглядно показывает логику действий декоратора по каждой из трех составляющих алгоритма его работы. Так, каждая строка вывода после вызова функции function1('Вася') подразумевает выполнение соответствующей составляющей из алгоритма работы декоратора. Здесь следует обратить внимание на третью (последнюю) строку в листинге, выводящее значение None. Такое значение просто объясняется тем, что наша функция function1() не имеет оператора return и, следовательно не выводит ни какого значения.

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

from math import sqrt

def dekorator_function(func):
    def wrapper(*args, **kwargs):
        print("Параметры вызова обертываемой функции -", args, kwargs)
        rezultat_function = func(*args, **kwargs)
        print("Возвращаемое значение функции -", rezultat_function)
        return rezultat_function
    return wrapper

@dekorator_function
def function2(a, b):
    return sqrt(a**2 + b**2)

function2(3, 4)
# Параметры вызова обертываемой функции - (3, 4) {}
# Возвращаемое значение функции - 5.0

Как видим, после обработки нашим декоратором функции function2() , последняя вывела лишь две строки вместо трех, как это было для function1(). Данное обстоятельство объясняется тем, что последняя наша функции ничего сама по себе не выводит на дисплей, она лишь возвращает рассчитанное ей значение через оператор return. Следовательно, вторая строка с выводом результатов работы функции function2(), в этом примере вообще отсутствует, но зато в третьей (второй) строке здесь появилось полноценное значение 5.0.

Заключение

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

Практический Python для начинающих
Практический Python для начинающих

Станьте junior Python программистом за 7 месяцев

 7 месяцев

Возможно будет интересно

🏆 Hello, world! Python
Новичок
🏆 Hello, world!

Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.

2022-10-04
Как практиковаться в Python? Python
Новичок
Как практиковаться в Python?

Для улучшения качества знаний и повышения уровня программиста, необходим постоянный практикум. Где можно это организовать самостоятельно, и как практиковаться в Python?

2022-10-19
Условные конструкции и сопоставление структурных шаблонов Шпаргалки
Новичок
Условные конструкции и сопоставление структурных шаблонов

Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов

2022-11-09