Декораторы в Python, часть 1

Oleksandr Shepetko7 ноября, 09:17 24 0

Краткое введение в декораторы Python

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

Функции

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

>>> def add_one(number):
...     return number + 1

>>> add_one(2)
3

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

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

Объекты первого класса

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

def say_hello(name):
    return f"Hello {name}"

def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def greet_bob(greeter_func):
    return greeter_func("Bob")

Здесь say_hello() и be_awesome() -- это обычные функции, принимающие строку на входе. А вот функция greet_bob() в качестве аргумента ожидает другую функцию, а не строку. То есть, например, можно в качестве аргумента передать функцию say_hello() или be_awesome():

>>> greet_bob(say_hello)
'Hello Bob'

>>> greet_bob(be_awesome)
'Yo Bob, together we are the awesomest!'

Обратите внимание на то, что greet_bob(say_hello) обращается к двум функциям, но делает это по-разному: greet_bob() и say_hello.  Обращение к функции say_hello происходит без использования круглых скобок, и это означает, что функция не будет вызвана, а вместо этого будет получена ссылка на саму функцию. А вот функция greet_bob(), вызванная привычным для нас способом, используя скобки, будет вызвана.

Вложенные функции

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

def parent():
    print("Printing from the parent() function")

    def first_child():
        print("Printing from the first_child() function")

    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()

Что будет выедено на экран в случае вызова функции parent()? Несложно догадаться, что мы получим:

>>> parent()
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function

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

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

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'first_child' is not defined

Возврат функций из функций

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

def parent(num):
    def first_child():
        return "Hi, I am Emma"

    def second_child():
        return "Call me Liam"

    if num == 1:
        return first_child
    else:
        return second_child

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

>>> first = parent(1)
>>> second = parent(2)

>>> first
<function parent.<locals>.first_child at 0x7f599f1e2e18>

>>> second
<function parent.<locals>.second_child at 0x7f599dad5268>

Эти несколько магические строки говорят нам о том, что в переменной first хранится ссылка на функцию first_child(), находящуюся в локальной области видимости функции parent(), а в переменной second, соответственно, ссылка на функцию second_child().

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

>>> first()
'Hi, I am Emma'

>>> second()
'Call me Liam'

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

Простые декораторы

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

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")

say_whee = my_decorator(say_whee)

Как думаете, что будет выведено в результате? Давайте попробуем:

>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

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

say_whee = my_decorator(say_whee)

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

>>> say_whee
<function my_decorator.<locals>.wrapper at 0x7f3c5dfd42f0>

При этом функция wrapper() хранит ссылку на исходную функцию say_whee(), что позволяет вызывать её между вызовами print().

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

Прежде чем двигаться дальше, давайте рассмотрим ещё один пример. Раз уж wrapper() является обыкновенной Python-функцией, она может изменять поведение декорируемой функции динамически. Скажем, нужно сделать так, что с 10 часов вечера до 7 утра функция say_whee() не должна работать:

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

def say_whee():
    print("Whee!")

say_whee = not_during_the_night(say_whee)

И теперь попробуйте вызвать её после того, как все отправились спать:

>>> say_whee()
>>>

Синтаксический сахар

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

Дабы избежать перечисленных неудобств, Python предлагет нам специальную конструкцию, иногда называемую pie-синтаксисом. Следующий пример полностью повторяет предыдущий, но уже с использованием синтаксиса декорации:

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_whee():
    print("Whee!")

Таким образом, @my_decorator -- это просто более простой способ описать конструкцию say_whee = my_decorator(say_whee).

Повторное использование декораторов

Надеюсь, вы ещё помните, что декоратор -- это обычная Python-функция, из чего следует, что мы можем делать с декораторами абсолютно всё то же самое, что и с функциями. Давайте поместим наш декоратор в отдельный модуль, чтобы его удобно было использовать из разных мест приложения. Создайте файл decorators.py и поместите в него код какого-нибудь полезного декоратора, например:

def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice

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

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

from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")
>>> say_whee()
Whee!
Whee!

Декорирование функций, принимающих аргументы

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

from decorators import do_twice

@do_twice
def greet(name):
    print(f"Hello {name}")
>>> greet("World")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given

К сожалению оказывается, это невозможно. Всё дело в том, что вложенная функция wrapper_do_twice() не принимает аргументов, в то время как ей передаётся аргумент name="World". Можно было бы добавить этот аргумент к функции wrapper_do_twice(), однако в этом случае она перестанет работать при декорировании функции say_whee(), описанной ранее.

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

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

Теперь все декорируемые функции будут работать как положено:

>>> say_whee()
Whee!
Whee!

>>> greet("World")
Hello World
Hello World

Возврат значений из декорируемых функций

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

from decorators import do_twice

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

Теперь попробуем использовать это:

>>> hi_adam = return_greeting("Adam")
Creating greeting
Creating greeting
>>> print(hi_adam)
None

Хм, наш декоратор где-то потерял возвращаемое функцией значение. Это вызвано тем, что do_twice_wrapper() не возвращает значение, получаемое от декорируемой функции. Исправить это, конечно же, очень просто, просто добавив return:

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice
>>> return_greeting("Adam")
Creating greeting
Creating greeting
'Hi Adam'

Кто ты на самом деле?

Одной из сильных сторон Python является интроспекция -- возможность получать доступ ко внутренней структуре объектов. В частности, например, функция "знает" о своём имени и тексте документации:

>>> print
<built-in function print>

>>> print.__name__
'print'

>>> help(print)
Help on built-in function print in module builtins:

print(...)
    <full help message>

Интроспекция также работает и для функций, объявленных разработчиком:

>> say_whee
<function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>

>>> say_whee.__name__
'wrapper_do_twice'

>>> help(say_whee)
Help on function wrapper_do_twice in module decorators:

wrapper_do_twice()

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

Исправить данную ситуацию можно относительно легко, прибегнув к помощи декоратора @functools.wraps, который сохраняет информацию о декорируемой функции. Этот декоратор использует функцию functools.update_wrapper() и с её помощью приводит в соответствие значения свойств __name__ и __doc__, которые используются при интроспекции.

import functools

def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice
>>> say_whee
<function say_whee at 0x7ff79a60f2f0>

>>> say_whee.__name__
'say_whee'

>>> help(say_whee)
Help on function say_whee in module whee:

say_whee()