Программирование на Python

Декораторы

Разбить на страницы
Показывать лекцию целиком

Последняя тема этой лекции - декораторы. Само слово декоратор подсказывает, что речь идет о каких-то дополнительных деталях. Есть спектакль и декорации к нему. Полагается, что декорации могут способствовать пониманию смысла спектакля, суть которого передает текст. Само слово "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("Превышен лимит")
    

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

    Декоратор с параметрами

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

    Давайте в нашем примере параметр limit зададим как параметр декоратора. Для этого построим обертку для декоратора decor1. Вот соответствующий код:

    def decor2(limit = 75):
        """ Обертка декоратора """
        def decor1(func):
            def wrapper(*args, **kwargs):
                if sum <= limit:
                   return 0
                else: return func(*args, **kwargs)
            return wrapper
        return decor1
        

    Заметьте, теперь limit в обертке wrapper - это не глобальная переменная, а параметр функции decor2. Сама функция decor2 декоратором не является, но ее результат является декоратором. Поэтому при декорировании функции можно указывать имя этой функции в качестве декоратора и задавать параметр limit в момент декорирования.

    Вот как выглядят теперь заголовки декорируемых функций в нашем примере:

    @decor2(limit = 40)
    def Expan10(goal = 'health'): 
    @decor2(limit = 50)
    def Expan30(goal = 'education'):
    @decor2()
    def Expan50(goal = 'family'): 
    

    Если запустить теперь тест 2, то при таком декорировании результаты будут следующими:

    Иногда полезно при построении декоратора внутреннюю обертку wrapper декорировать встроенным декоратором wraps:

    from functools import wraps
    @wraps
    def wrapper(*args, **kwargs):
    

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

    Декоратор Timer

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

    def timer(repeat = 1):
        def decor_timer(func):
            """измеряет время работы функции
            при вызове функции repeat раз"""
            
            from time import time
            def wrapper(*args, **kwargs):
                start = time()
                for i in range(repeat):
                    res = func(*args, **kwargs)
                fin = time()
                L = []
                L.append(fin - start)
                L.append(res)
                return L
            return wrapper
        return decor_timer
    

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

    @timer( repeat = 100)
    def CreateList(n):
        L = []
        for num in range(n):
            if (num % 3 == 0 and num % 5 == 0 
                and num % 13 == 0 and num % 31 == 0 and num % 2 != 0) :
                L.append(num * num)
        return L
    
    @timer(repeat = 100)
    def Gen_CreateList(n):
        L = [num * num for num in range(n) if (num % 3 == 0 and num % 5 == 0 
                and num % 13 == 0 and num % 31 == 0 and num % 2 != 0)]
        return L  
    

    Вот тест, запускающий эти методы на выполнение:

    def test3():
        res = CreateList(1000000)
        print (res)
        print()
        print()
        res = Gen_CreateList(1000000)
        print (res)
    

    Приведу результаты работы теста:

    Что показывают эти результаты? Стократное создание списка с исходным списком в миллион элементов оба метода выполняют за время меньшее 6 секунд. Более важно, - рекламируемая эффективность генераторов в сравнении с классическим кодом не подтверждается результатами измерений.

    Давайте проверим эффективность функций-генераторов. На прошлой лекции мы построили три варианта сложения матриц. Приведу код этих методов с небольшой вариацией:

    @timer(repeat = 1)
    def SumMatr(A, B):
        """Сложение квадратных матриц """
        n = len(A)
        LL = []
        for row in range(n):
            L = []
            for col in range(n):            
                 L.append(A[row][col] + B[row][col])
            LL.append(L)
        return LL
    
    @timer(repeat = 1)
    def GenSumMatr(A, B):
        """Сложение квадратных матриц.Генераторы списков"""
        n = len(A)
        L = [[A[row][col] + B[row][col]
              for col in range(n)] for row in range(n)]
        return L
    
    def FunGenSumMatr(A, B):
        """
        Сложение квадратных матриц. Функция-генератор
        При каждом вызове возвращается 
        очередная строка суммарной матрицы.
        Возвращаемое значение является генератором списка
        """
        n = len(A)
        for row in range(n):
            yield [A[row][col] + B[row][col] for col in range(n)]
    
    @timer(repeat = 1)
    def ExprSumMatr(A, B):
        """Сложение квадратных матриц. Функция-генератор"""
        n = len(A)    
        L = []
        for value in FunGenSumMatr(A, B)
            L.append(value)
        return L
            

    Первые два метода возвращают в качестве результата список, представляющий сумму матриц. Функция-генератор FunGenSumMatr выдает список, представляющий строку суммарной матрицы. Метод ExprSumMatr, создает конечный список по мере поступления строк.

    Приведу тест, позволяющий проводить замеры времени работы этих трех методов:

    def test4():
        A = CreateMatr(1000)
        B = CreateMatr(1000)
    
        res = SumMatr(A, B)
        print("Сложение матриц. Классический вариант: n =  ", len(A),
             ' время выполнения = ', res[0])
    
        res = GenSumMatr(A, B)
        print("Сложение матриц. Генератор: n =  ", len(A),
             ' время выполнения = ', res[0])
    
        res = ExprSumMatr(A, B)
        print("Сложение матриц. Функция-генератор: n =  ", len(A),
             ' время выполнения = ', res[0])
    

    Вот результаты работы этого теста:

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

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

    Декорировать можно не только функции. В языке Python есть возможность декорировать классы, добавляя к ним новые методы. Классы Python - это такие же объекты, как и прочие объекты, следовательно могут изменяться динамически. Язык Python позволяет работать с метаклассами, использующими декорирование классов. Эти инструменты используются в специальных сложных проектах и их рассмотрение выходит за пределы основ программирования.

    Страницы:

    Последняя тема этой лекции - декораторы. Само слово декоратор подсказывает, что речь идет о каких-то дополнительных деталях. Есть спектакль и декорации к нему. Полагается, что декорации могут способствовать пониманию смысла спектакля, суть которого передает текст. Само слово "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("Превышен лимит")
    

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

    Декоратор с параметрами

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

    Давайте в нашем примере параметр limit зададим как параметр декоратора. Для этого построим обертку для декоратора decor1. Вот соответствующий код:

    def decor2(limit = 75):
        """ Обертка декоратора """
        def decor1(func):
            def wrapper(*args, **kwargs):
                if sum <= limit:
                   return 0
                else: return func(*args, **kwargs)
            return wrapper
        return decor1
        

    Заметьте, теперь limit в обертке wrapper - это не глобальная переменная, а параметр функции decor2. Сама функция decor2 декоратором не является, но ее результат является декоратором. Поэтому при декорировании функции можно указывать имя этой функции в качестве декоратора и задавать параметр limit в момент декорирования.

    Вот как выглядят теперь заголовки декорируемых функций в нашем примере:

    @decor2(limit = 40)
    def Expan10(goal = 'health'): 
    @decor2(limit = 50)
    def Expan30(goal = 'education'):
    @decor2()
    def Expan50(goal = 'family'): 
    

    Если запустить теперь тест 2, то при таком декорировании результаты будут следующими:

    Иногда полезно при построении декоратора внутреннюю обертку wrapper декорировать встроенным декоратором wraps:

    from functools import wraps
    @wraps
    def wrapper(*args, **kwargs):
    

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

    Декоратор Timer

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

    def timer(repeat = 1):
        def decor_timer(func):
            """измеряет время работы функции
            при вызове функции repeat раз"""
            
            from time import time
            def wrapper(*args, **kwargs):
                start = time()
                for i in range(repeat):
                    res = func(*args, **kwargs)
                fin = time()
                L = []
                L.append(fin - start)
                L.append(res)
                return L
            return wrapper
        return decor_timer
    

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

    @timer( repeat = 100)
    def CreateList(n):
        L = []
        for num in range(n):
            if (num % 3 == 0 and num % 5 == 0 
                and num % 13 == 0 and num % 31 == 0 and num % 2 != 0) :
                L.append(num * num)
        return L
    
    @timer(repeat = 100)
    def Gen_CreateList(n):
        L = [num * num for num in range(n) if (num % 3 == 0 and num % 5 == 0 
                and num % 13 == 0 and num % 31 == 0 and num % 2 != 0)]
        return L  
    

    Вот тест, запускающий эти методы на выполнение:

    def test3():
        res = CreateList(1000000)
        print (res)
        print()
        print()
        res = Gen_CreateList(1000000)
        print (res)
    

    Приведу результаты работы теста:

    Что показывают эти результаты? Стократное создание списка с исходным списком в миллион элементов оба метода выполняют за время меньшее 6 секунд. Более важно, - рекламируемая эффективность генераторов в сравнении с классическим кодом не подтверждается результатами измерений.

    Давайте проверим эффективность функций-генераторов. На прошлой лекции мы построили три варианта сложения матриц. Приведу код этих методов с небольшой вариацией:

    @timer(repeat = 1)
    def SumMatr(A, B):
        """Сложение квадратных матриц """
        n = len(A)
        LL = []
        for row in range(n):
            L = []
            for col in range(n):            
                 L.append(A[row][col] + B[row][col])
            LL.append(L)
        return LL
    
    @timer(repeat = 1)
    def GenSumMatr(A, B):
        """Сложение квадратных матриц.Генераторы списков"""
        n = len(A)
        L = [[A[row][col] + B[row][col]
              for col in range(n)] for row in range(n)]
        return L
    
    def FunGenSumMatr(A, B):
        """
        Сложение квадратных матриц. Функция-генератор
        При каждом вызове возвращается 
        очередная строка суммарной матрицы.
        Возвращаемое значение является генератором списка
        """
        n = len(A)
        for row in range(n):
            yield [A[row][col] + B[row][col] for col in range(n)]
    
    @timer(repeat = 1)
    def ExprSumMatr(A, B):
        """Сложение квадратных матриц. Функция-генератор"""
        n = len(A)    
        L = []
        for value in FunGenSumMatr(A, B)
            L.append(value)
        return L
            

    Первые два метода возвращают в качестве результата список, представляющий сумму матриц. Функция-генератор FunGenSumMatr выдает список, представляющий строку суммарной матрицы. Метод ExprSumMatr, создает конечный список по мере поступления строк.

    Приведу тест, позволяющий проводить замеры времени работы этих трех методов:

    def test4():
        A = CreateMatr(1000)
        B = CreateMatr(1000)
    
        res = SumMatr(A, B)
        print("Сложение матриц. Классический вариант: n =  ", len(A),
             ' время выполнения = ', res[0])
    
        res = GenSumMatr(A, B)
        print("Сложение матриц. Генератор: n =  ", len(A),
             ' время выполнения = ', res[0])
    
        res = ExprSumMatr(A, B)
        print("Сложение матриц. Функция-генератор: n =  ", len(A),
             ' время выполнения = ', res[0])
    

    Вот результаты работы этого теста:

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

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

    Декорировать можно не только функции. В языке Python есть возможность декорировать классы, добавляя к ним новые методы. Классы Python - это такие же объекты, как и прочие объекты, следовательно могут изменяться динамически. Язык Python позволяет работать с метаклассами, использующими декорирование классов. Эти инструменты используются в специальных сложных проектах и их рассмотрение выходит за пределы основ программирования.

    Вернуться к учебному плану