Изменение поведения функции при работе с декораторами в Python (часть 1)

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

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

Обзор простых декораторов Python

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

def my_decorator(f):

    print('decorating', f)

    return f

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

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

>>> @my_decorator

... def my_function(a, b):

...     return a + b

...

decorating <function my_function at 0x10ae241e0>

>>> my_function(1, 2)

3

>>>

Посмотрите на этот пример. Видите, что текст decorating… мы указали. Он появляется в ходе определения функции, а не во время вызова. На этот момент необходимо обратить особое внимание во время разработки приложения.

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

Этот декоратор никаких действий не предпринимает. Следовательно, функция my_function() также не изменяется, и ее вызов может осуществляться стандартным способом.

Какие декораторы заменяют декорированные функции?

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

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

Давайте сначала попробуем изменить оператор return, чтобы им возвращалась функция, отличающаяся от f. Давайте попробуем вернуться к оболочке Python. 

def forty_two():

    return 42

 

def my_decorator(f):

    print('decorating', f)

    return forty_two

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

@my_decorator

def my_function(a, b):

    return a + b

Ну и, наконец, получаем следующий результат. 

decorating <function my_function at 0x10ae24268>

>>> my_function(1, 2)

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: forty_two() takes 0 positional arguments but 2 were given

>>>

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

Учтите, что возвращаемая декоратором функция заменяет исходную. Фактически декоратор изменяет функцию my_function, чтобы она ссылалась на forty_two. Следовательно, мы получили ошибку, когда попытались вызвать my_function(1,2). На самом деле, произошел вызов другой функции – forty_two(1,2). Конечно, в нашем случае это вообще не нужно, поскольку функцией не принимаются никакие аргументы. 

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

Что делать для того, чтобы избавиться от этой ошибки? Так, как теперь my_function – это название для forty_two, необходимо эту функцию вызвать без аргументов. 

>>> my_function()

42

У вас может появиться вопрос: а где на данный момент исходная функция расположена? В описанном нами примере, к сожалению, исходной функции больше нет. Все из-за того, что декоратором она была заменена на forty_two().

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

Что же делать в такой ситуации? В этом случае давайте рассмотрим такой термин, как «внутренняя функция». Для начала посмотрите на этот декоратор. 

def my_decorator(f):

    def wrapped():

        return f(1, 2)

    print('decorating', f)

    return wrapped

В данном случае функция wrapped() является внутренней, поскольку ее определение осуществляется внутри тела другой функции. 

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

Посмотрите на этот пример и обратите внимание на то, что внутри тела функции wrapped() есть возможность осуществить вызов f(1,2), хотя при этом внутри функции она не была определена. Вместо этого, она выполняет роль аргумента родительской функции my_decorator().

А теперь давайте его проверим. Для этого выполним такой небольшой фрагмент кода. 

@my_decorator

def my_function(a, b):

    return a + b

В результате выполнения этого кода получится следующая ошибка. 

decorating <function my_function at 0x10ae24378>

>>> my_function(1, 2)

 

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: wrapped() takes 0 positional arguments but 2 were given

 

>>> my_function()

3

Любопытно, не так ли? Мы, как и раньше, не можем передать аргументы a и b оригинальной функции my_function(). Тем не менее, если мы ее вызываем, то получаем значение 3, так как функция wrapped() внедряет аргументы 1 и 2.

Для полного восстановления двух аргументов из исходной функции, мы можем сделать следующее. А именно, определить функцию wrapped() с двумя аргументами и передать их в f

def my_decorator(f):

    def wrapped(a, b):

        return f(a, b)

    print('decorating', f)

    return wrapped

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

В Python также можно создать функцию для принятия любых аргументов со специальными аргументами *args и **kwargs, которые отвечают за позиционные аргументы и аргументы ключевых слов соответственно. 

def my_decorator(f):

    def wrapped(*args, **kwargs):

        return f(*args, **kwargs)

    print('decorating', f)

    return wrapped

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

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

Ознакомьтесь с этим примером и почитайте комментарии к нему. Они показывают, где именно в декораторе возможна вставка новых вариантов поведения. 

def my_decorator(f):

    def wrapped(*args, **kwargs):

        # ...

        # вставляется код, который запускается перед декорированной функцией

        # (и опционально можно не вызывать данную функцию)

        # ...

        response = f(*args, **kwargs)

        # ...

        # вставляется код, который запускается после декорированной функцией

        # (и опционально решается, менять ли ответ)

        # ...

        return response

    return wrapped

Чтобы было проще визуализировать, каким образом осуществляется работа этого стиля декоратора, приведем еще один пример. 

def my_decorator(f):

    def wrapped(*args, **kwargs):

        print('До функции')

        response = f(*args, **kwargs)

        print('После функции')

        return response

    print('декорируем', f)

    return wrapped

 

@my_decorator

def my_function(a, b):

    print('В функции')

    return a + b

Выводы

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

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

ОфисГуру
Adblock
detector