НОЧУ ДПО "Национальный открытый университет "ИНТУИТ"
Опубликован: 24.01.2021 | Доступ: свободный | Студентов: 2489 / 106 | Длительность: 03:57:00
Лекция 31:

Декораторы

< Лекция 1 || Лекция 31: 123

Смотреть на youtube

Последняя тема этой лекции - декораторы. Само слово декоратор подсказывает, что речь идет о каких-то дополнительных деталях. Есть спектакль и декорации к нему. Полагается, что декорации могут способствовать пониманию смысла спектакля, суть которого передает текст. Само слово "decoration" в переводе с английского означает "украшать, награждать кого-либо орденом".

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

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

Декорирование функций

Рассмотрим следующую ситуацию. Пусть в созданном нами модуле или классе есть несколько функций f_1, f_2, \dots f_n. В ходе эксплуатации или проектирования выяснилось, что этим функциям нужно добавить некоторую общую функциональность. Как справиться с этой задачей? Простое решение - добавить соответствующий код в каждую из функций. Такое решение нарушает два важных принципа программирования:

  • Следует избегать повторяющегося кода в разных участках программы. Повторяющийся код можно оформить в виде некоторой функции и вызывать эту функцию в точках повторения кода. Можно использовать наследование или другие приемы, позволяющие справиться с дублированием. Декорирование один из таких приемов.
  • Не следует изменять корректно работающую функцию. Лучшим приемом является создание новой функции, которая использует работающую функцию, добавляя к ней новую функциональность. Именно это и делает декоратор Python .

Если у нас есть работающая функция f и мы хотим добавить функциональность или изменить ее интерфейс, то один из способов - это построение новой функции g, представляющей обертку функции f:

g = wrapper(f)

Приведу простой уже встречавшийся пример построения обертки. Для сортировки массива строился рекурсивный метод QSort(a, start, finish), имеющий три аргумента -сортируемый массив и два аргумента, задающие интервал сортировки. Исходно требуется сортировать массив, поэтому целесообразно построить нерекурсивную обертку рекурсивного метода:

def QuickSort(a):
    QSort(a, 0, len(a))

У обертки в данном случае проще интерфейс - один аргумент, а не три. Все, что делает обертка, - это вызов рекурсивной функции.

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

Какое же решение предлагает язык Python для решения возникшей у нас проблемы - добавления новой функциональности функциям f_1, f_2, \dots f_n? Python позволяет построить декоратор, который добавляет новую функциональность.

Давайте рассмотрим, как строится декоратор. Декоратор Python - это функция, аргументом которой является декорируемая функция. Заголовок декоратора, следовательно имеет вид:

def decor(func):
    """ Декоратор """  

Здесь d?cor - имя декоратора, func - имя декорируемой функции.

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

def decor(func):
    """ Декоратор """  
       def wrapper(*args, **kwargs):
        """ функция обертка """
	… код обертки …  
   return wrapper

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

Два важных момента, связанных с декораторами:

  • Декоратор - это функция, но декоратор не вызывается обычным способом. Вместо вызова декоратора используется декорирование функции. Синтаксически это выглядит так:

    @decor
    def Expances10(goal = 'health'): 

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

  • Когда при выполнении кода встречается вызов декорируемой функции, которой в точке вызова передаются фактические аргументы, то автоматически происходит замена вызова декорируемой функции на вызов функции-обертки, которой передаются фактические аргументы. Поскольку набор аргументов у обертки и у декорируемой функции совпадают, то никаких проблем в результате замены не возникает.

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

Начнем строить код нашего модуля:

""" Модуль: Планирование бюджета семьи """
sum = 100   #Выделенная сумма

Здесь sum - атрибут, задающий бюджет, объявлен на глобальном уровне и доступен всем методам модуля. Приведу теперь методы, ведающие расходами и доходами бюджета:

def Income(inc):
    global sum
    sum += inc
@decor
def Expances10(goal = 'health'):
    global sum
    res = 0.1 * sum
    sum -= res
    #сумма res используется для укрепления здоровья
    
@decor
def Expances30(goal = 'education'):
    global sum
    res = 0.3 * sum
    sum -= res
    #сумма res используется на образование
    
@decor
def Expances50(goal = 'family'):
    global sum
    res = 0.5 * sum
    sum -= res
    #сумма res используется для удовлетворения потребностей членов семьи

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

В текст модуля добавили глобальный аргумент:

  limit = 75  #ограничитель расходов.

Для добавления функциональности построили декоратор d?cor и декорировали им расходные функции. Вот код декоратора d?cor:

def decor(func):
    """ Декоратор процедур
    вызывает декорируемую процедуру, 
    если таможня дает добро
    """
    def wrapper(*args, **kwargs):
        if sum <= limit:
           print ("Превышен лимит")
        else: func(*args, **kwargs)
    return wrapper

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

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

  • *args означает, что декорируемая функция в точке вызова может иметь список позиционных параметров неограниченной длины, возможно, пустой.
  • **kwargs (key word arguments) означает, что декорируемая функция в точке вызова может иметь список именованных параметров неограниченной длины, возможно, пустой.
  • Совпадение заголовков функций wrapper и func обеспечивает эквивалентность списков аргументов этих функций.

Построим тест, проверяющий работу декорируемых функций:

def test1():

    sum1 = sum
    Expances10()
    if sum1 > sum:
        print("потрачено на здоровье - ", sum1 - sum )
    
    sum1 = sum
    Expances30()
    if sum1 > sum:
        print("потрачено на образование - ", sum1 - sum )

    sum1 = sum
    Expances50()
    if sum1 > sum:
        print("потрачено на семью - ", sum1 - sum )

При sum = 100 и limit = 75 этот тест дает следующие результаты:


На лекарства и образование денег хватило, а на покупки по запросам членов семьи денег в бюджете недостаточно.

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

@decor1
def Expan10(goal = 'health'):
    global sum
    res = 0.1 * sum
    sum -= res
    return res
@decor1
def Expan30(goal = 'education'):
    global sum
    res = 0.3 * sum
    sum -= res
    return res
@decor1
def Expan50(goal = 'family'):
    global sum
    res = 0.5 * sum
    sum -= res
    return res

Эти функции декорированы другим декоратором decor1. Вот его текст:

def decor1(func):
    """ Декоратор функций:
    возвращает результат вызова декорируемой функцию, 
    если таможня дает добро,
    иначе возвращает ноль
    """ 
    def wrapper(*args, **kwargs):
        if sum <= limit:
           return 0
        else: return func(*args, **kwargs)
    return wrapper

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

Приведу тест, выполняющий проверку корректности работы новых декорированных функций:

def test2():
    res =Expan50()
    if res > 0:
        print("потрачено на семью - ", res )
    else:
        print("Превышен лимит")
    res = Expan10()
    if res > 0:
        print("потрачено на здоровье - ", res )
    else:
        print("Превышен лимит")
    res = Expan30()
    if res > 0:
        print("потрачено на образование - ", res )
    else:
 print("Превышен лимит")

В этом тесте запросы членов семьи были удовлетворены, но на здоровье и образование денег уже не хватило.


< Лекция 1 || Лекция 31: 123
Алексей Авилов
Алексей Авилов

Неужели не нашлось русских специалистов, чтобы записать курс по пайтону ? Да, можно включить переводчик и слушать с переводом, но это что? Это кто-то считает хорошим и понятным курсом для начинающих? 

Елена Лаптева
Елена Лаптева

Думаю. что не смогу его закончить. Хотелось предупредить других - не тратьте зря время, ищите другой курс.