Декораторы в  Python

Декораторы — это, по сути, “обёртки”, которые дают нам возможность изменить поведение функции, не изменяя её код.

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

Что такое декоратор?

Новичкам декораторы могут показаться неудобными и непонятными, потому что они выходят за рамки «обычного» процедурного программирования как в Си, где вы объявляете функции, содержащие блоки кода, и вызываете их. То же касается и объектно-ориентированного программирования, где вы определяете классы и создаёте на их основе объекты. Декораторы не принадлежат ни одной из этих парадигм и исходят из области функционального программирования. Однако не будем забегать вперёд, разберёмся со всем по порядку.

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

Как работают функции

Все мы знаем, что такое функции, не так ли? Не будьте столь уверены в этом. У функций Python есть определённые аспекты, с которыми мы нечасто имеем дело, и, как следствие, они забываются. Давайте проясним, что такое функции и как они представлены в Python.

Функции как процедуры

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

Функции как объекты первого класса

В Python всё является объектом, а не только объекты, которые вы создаёте из классов. В этом смысле он (Python) полностью соответствует идеям объектно-ориентированного программирования. Это значит, что в Python всё это — объекты:

  • числа;
  • строки;
  • классы (да, даже классы!);
  • функции (то, что нас интересует).

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

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

И тут в дело вступает функциональное программирование, а вместе с ним — декораторы.

Функциональное программирование — функции высших порядков

В Python используются некоторые концепции из функциональных языков вроде Haskell и OCaml. Пропустим формальное определение функционального языка и перейдём к двум его характеристикам, свойственным Python:

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

Функциональному программированию присущи и другие свойства вроде отсутствия побочных эффектов, но мы здесь не за этим. Лучше сконцентрируемся на другом — функциях высших порядков. Что есть функция высшего порядка? Снова обратимся к Википедии:

Функции высших порядков — это такие функции, которые могут принимать в качестве аргументов и возвращать другие функции.

Если вы знакомы с основами высшей математики, то вы уже знаете некоторые математические функции высших порядков порядка вроде дифференциального оператора d/dx. Он принимает на входе функцию и возвращает другую функцию, производную от исходной. Функции высших порядков в программировании работают точно так же — они либо принимают функцию(и) на входе и/или возвращают функцию(и).

Пара примеров

Раз уж мы ознакомились со всеми аспектами функций в Python, давайте продемонстрируем их в коде:

def hello_world():
    print('Hello world!')

Здесь мы определили простую функцию. Из фрагмента кода далее вы увидите, что эта функция, как и классы с числами, является объектом в Python:

>>> def hello_world():
...     print('Hello world!')
...
>>> type(hello_world)
<class 'function'>
>>> class Hello:
...     pass
...
>>> type(Hello)
<class 'type'>
>>> type(10)
<class 'int'>

Как вы заметили, функция hello_world принадлежит типу <class 'function'>. Это означает, что она является объектом класса function. Кроме того, класс, который мы определили, принадлежит классу type. От этого всего голова может пойти кругом, но чуть поигравшись с функцией type вы со всем разберётесь.

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

Мы можем хранить функции в переменных:

>>> hello = hello_world
>>> hello()
Hello world!

Определять функции внутри других функций:

>>> def wrapper_function():
...     def hello_world():
...         print('Hello world!')
...     hello_world()
...
>>> wrapper_function()
Hello world!

Передавать функции в качестве аргументов и возвращать их из других функций:

>>> def higher_order(func):
...     print('Получена функция {} в качестве аргумента'.format(func))
...     func()
...     return func
...
>>> higher_order(hello_world)
Получена функция <function hello_world at 0x032C7FA8> в качестве аргумента
Hello world!
<function hello_world at 0x032C7FA8>

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

Как работают декораторы

Повторим определение декоратора:

Декоратор — это функция, которая позволяет обернуть другую функцию для расширения её функциональности без непосредственного изменения её кода.

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

def decorator_function(func):
    def wrapper():
        print('Функция-обёртка!')
        print('Оборачиваемая функция: {}'.format(func))
        print('Выполняем обёрнутую функцию...')
        func()
        print('Выходим из обёртки')
    return wrapper

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

>>> @decorator_function
... def hello_world():
...     print('Hello world!')
...
>>> hello_world()
Оборачиваемая функция: <function hello_world at 0x032B26A8>
Выполняем обёрнутую функцию...
Hello world!
Выходим из обёртки

Магия, не иначе! Просто добавив @decorator_function перед определением функции hello_world(), мы модифицировали её поведение. Однако как вы уже могли догадаться, выражение с @ является всего лишь синтаксическим сахаром для hello_world = decorator_function(hello_world).

Иными словами, выражение @decorator_function вызывает decorator_function() с hello_world в качестве аргумента и присваивает имени hello_world возвращаемую функцию.

И хотя этот декоратор мог вызвать вау-эффект, он не очень полезный. Давайте взглянем на другие, более полезные (наверное):

def benchmark(func):
    import time

    def wrapper():
        start = time.time()
        func()
        end = time.time()
        print('[*] Время выполнения: {} секунд.'.format(end-start))
    return wrapper

@benchmark
def fetch_webpage():
    import requests
    webpage = requests.get('https://google.com')

fetch_webpage()

Здесь мы создаём декоратор, замеряющий время выполнения функции. Далее мы используем его на функции, которая делает GET-запрос к главной странице Google. Чтобы измерить скорость, мы сначала сохраняем время перед выполнением обёрнутой функции, выполняем её, снова сохраняем текущее время и вычитаем из него начальное.

После выполнения кода получаем примерно такой результат:

[*] Время выполнения: 1.4475083351135254 секунд.

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

Используем аргументы и возвращаем значения

В приведённых выше примерах декораторы ничего не принимали и не возвращали. Модифицируем наш декоратор для измерения времени выполнения:

def benchmark(func):
    import time

    def wrapper(*args, **kwargs):
        start = time.time()
        return_value = func(*args, **kwargs)
        end = time.time()
        print('[*] Время выполнения: {} секунд.'.format(end-start))
        return return_value
    return wrapper

@benchmark
def fetch_webpage(url):
    import requests
    webpage = requests.get(url)
    return webpage.text

webpage = fetch_webpage('https://google.com')
print(webpage)

Вывод после выполнения:

[*] Время выполнения: 1.4475083351135254 секунд.
<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage"........

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

Декораторы с аргументами

Мы также можем создавать декораторы, которые принимают аргументы. Посмотрим на пример:

def benchmark(iters):
    def actual_decorator(func):
        import time

        def wrapper(*args, **kwargs):
            total = 0
            for i in range(iters):
                start = time.time()
                return_value = func(*args, **kwargs)
                end = time.time()
                total = total + (end-start)
            print('[*] Среднее время выполнения: {} секунд.'.format(total/iters))
            return return_value

        return wrapper
    return actual_decorator


@benchmark(iters=10)
def fetch_webpage(url):
    import requests
    webpage = requests.get(url)
    return webpage.text

webpage = fetch_webpage('https://google.com')
print(webpage)

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

Функция benchmark() на первый взгляд может показаться декоратором, но на самом деле таковым не является. Это обычная функция, которая принимает аргумент iters, а затем возвращает декоратор. В свою очередь, он декорирует функцию fetch_webpage(). Поэтому мы использовали не выражение @benchmark, а @benchmark(iters=10) — это означает, что тут вызывается функция benchmark() (функция со скобками после неё обозначает вызов функции), после чего она возвращает сам декоратор.

Да, это может быть действительно сложно уместить в голове, поэтому держите правило:

Декоратор принимает функцию в качестве аргумента и возвращает функцию.

В нашем примере benchmark() не удовлетворяет этому условию, так как она не принимает функцию в качестве аргумента. В то время как функция actual_decorator(), которая возвращается benchmark(), является декоратором.

Объекты-декораторы

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

from collections import deque

class Memoized:
    def __init__(self, cache_size=100):
        self.cache_size = cache_size
        self.call_args_queue = deque()
        self.call_args_to_result = {}

    def __call__(self, fn):
        def new_func(*args, **kwargs):
            memoization_key = self._convert_call_arguments_to_hash(args, kwargs)
            if memoization_key not in self.call_args_to_result:
                result = fn(*args, **kwargs)
                self._update_cache_key_with_value(memoization_key, result)
                self._evict_cache_if_necessary()
            return self.call_args_to_result[memoization_key]
        return new_func

    def _update_cache_key_with_value(self, key, value):
        self.call_args_to_result[key] = value
        self.call_args_queue.append(key)

    def _evict_cache_if_necessary(self):
        if len(self.call_args_queue) > self.cache_size:
            oldest_key = self.call_args_queue.popleft()
            del self.call_args_to_result[oldest_key]

    @staticmethod
    def _convert_call_arguments_to_hash(args, kwargs):
        return hash(str(args) + str(kwargs))


@Memoized(cache_size=5)
def get_not_so_random_number_with_max(max_value):
    import random
    return random.random() * max_value

Само собой, этот декоратор нужен в основном в демонстрационных целях, в реальном приложении для подобного кеширования стоит использовать functools.lru_cache.

P.S.

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

  • Декораторы не обязательно должны быть функциями, это может быть любой вызываемый объект.
  • Декораторы не обязаны возвращать функции, они могут возвращать что угодно. Но обычно мы хотим, чтобы декоратор вернул объект того же типа, что и декорируемый объект. Пример:>>> def decorator(func): ... return 'sumit' ... >>> @decorator ... def hello_world(): ... print('hello world') ... >>> hello_world 'sumit'
  • Также декораторы могут принимать в качестве аргументов не только функции. Здесь можно почитать об этом подробнее.
  • Необходимость в декораторах может быть неочевидной до написания библиотеки. Поэтому, если декораторы кажутся вам бесполезными, посмотрите на них с точки зрения разработчика библиотеки. Хорошим примером является декоратор представления в Flask.
  • Также стоит обратить внимание на functools.wraps() — функцию, которая помогает сделать декорируемую функцию похожей на исходную, делая такие вещи, как сохранение doctstring исходной функции.

Другая статья про декораторы

1. Предварительные соображения: функции

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

1.1. Передача функции в качестве аргумента

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

def say_hello(name):
    return f"Привет, {name}!"

def be_awesome(name):
    return f"Класс, {name}, быть вместе так круто!"

def greet_vanya(greeter_func):
    return greeter_func("Ваня")
    

Здесь say_hello() и be_awesome() – обычные функции, которые получают строковую переменную name. Функция greet_vanya() в качестве аргумента получает другую функцию, например say_hello() или be_awesome():

 >>> greet_vanya(say_hello)
'Привет, Ваня!'

>>> greet_vanya(be_awesome)
'Класс, Ваня, быть вместе так круто!'
    

При передаче в качестве аргумента имя функции указывается без скобок – передаётся только ссылка на функцию. Сама функция не выполняется, пока не будет вызвана функция greet_vanya().

1.2. Внутренние функции

Функции, определенные внутри других функций, называются внутренними (inner functions). Пример функции с двумя внутренними функциями:

def parent():
    print("Привет из функции parent().")

    def first_child():
        print("Привет из функции first_child().")

    def second_child():
        print("Привет из функции second_child().")

    second_child()
    first_child()
    

Что произойдёт при вызове функции parent()? Остановитесь, чтобы подумать. Вывод будет следующим:

>>> parent()
Привет из функции parent().
Привет из функции second_child().
Привет из функции first_child().
    

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

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

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

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

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

def parent(num):
    def first_child():
        return "Привет, меня зовут Ксавье."

    def second_child():
        return "Зови меня X Æ A-12."

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

В инструкции return возвращается ссылка на функцию, то есть имя функции указывается без скобок (иначе бы возвращался результат выполнения функции).

>>> first = parent(1)
>>> second = parent(2)
<function __main__.parent.<locals>.first_child()>
>>> second
<function __main__.parent.<locals>.second_child()>

    

В приведенном примере first и second – переменные, в которые были записаны ссылки на локальные функции first_child() и second_child() внутри функции parent(). Теперь first и second можно использовать как обычные функции, хотя функции, на которые они указывают, недоступны напрямую:

>>> first()
'Привет, меня зовут Ксавье.'
>>> second()
'Зови меня X Æ A-12.'
    

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

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

2.1. Общая идея: используем знания о функциях

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

def my_decorator(func):
    def wrapper():
        print("До вызова функции.")
        func()
        print("После вызова функции.")
    return wrapper

def say_whee():
    print("Ура!")

say_whee = my_decorator(say_whee)
    

Знаете, что произойдёт при вызове say_whee()?

>>> say_whee()
До вызова функции.
Ура!
После вызова функции.
    

Чтобы понять, что происходит, оглянемся на предыдущие примеры. Мы просто применяем всё, что узнали до сих пор. Декорирование происходит в последней строчке:

        say_whee = my_decorator(say_whee)
    

Мы передаем в функцию my_decorator() ссылку на функцию say_whee. В my_decorator()есть внутренняя функция wrapper(), ссылка на которую возвращается в инструкции return внешней функции. В результате мы передали в my_decorator() в качестве аргумента ссылку на одну функцию, а назад получили ссылку на её функцию-обёртку.

Теперь имя say_whee указывает на внутреннюю функцию wrapper:

>>> say_whee
<function __main__.my_decorator.<locals>.wrapper()>
    

Однако wrapper() содержит ссылку на оригинал say_whee() и вызывает эту функцию между двумя вызовами print().

Проще говоря

Декоратор обертывает функцию, изменяя ее поведение.

Добавим динамики. Рассмотрим второй пример, иллюстрирующий динамическое поведение декораторов. Сделаем так, чтобы наша функция кричала “Ура!” только в дневное время.

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 8 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Тише, соседи спят!
    return wrapper

def say_whee():
    print("Ура!")

say_whee = not_during_the_night(say_whee)
    

Декорированная функция say_whee() будет выводить "Ура" только, если она запущена в интервале c 8:00 до 22:00 (чтобы проверить разницу в поведении, «подкрутите стрелки» ⏰).

2.2. Немного синтаксического сахара!

То, как мы декорировали say_whee(), прямо скажем, выглядит неуклюже. В последнем примере мы три раза использовали имя say_whee: при определении функции-оригинала, при передаче ссылку в функцию not_during_the_night() и при переопределении имени для создания ссылки на декоратор.

Чтобы не заниматься такими глупостями, в Python можно создать декоратор с помощью символа @. Следующий код эквивалентен первому рассмотренному примеру:

def my_decorator(func):
    def wrapper():
        print("До вызова функции.")
        func()
        print("После вызова функции.")
    return wrapper

@my_decorator
def say_whee():
    print("Ура!")
    
>>> say_whee()
До вызова функции.
Ура!
После вызова функции.
    

То есть инструкция @my_decorator, идущая перед определением функции say_whee() эквивалентна инструкции say_whee = my_decorator(say_whee).

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

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

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

Теперь импортируем функцию из модуля и используем как декоратор:

from decorators import do_twice

@do_twice
def say_whee():
    print("Ура!")
    

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

>>> say_whee()
Ура!
Ура!
    

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

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

from decorators import do_twice

@do_twice
def greet(name):
    print(f"Привет, {name}!")
    

К сожалению, запуск кода вызовет ошибку:

>>> greet("мир")
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() не принимает аргументов. Нужно добавить их обработку. Перепишем decorators.py следующим образом:decorators.py

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

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

>>> say_whee()
Ура!
Ура!

>>> greet("мир")
Привет, мир!
Привет, мир!
    

2.5. Возвращение значения из декорированных функций

В декораторе можно описать, что делать со значением, возвращаемым декорированной функцией:

from decorators import do_twice

@do_twice
def return_greeting(name):
    print("Готовлюсь приветствовать...")
    return f"Привет, {name}!"
    

Попытаемся использовать декорированную функцию:

>>> hi_adam = return_greeting("Адам")
Готовлюсь приветствовать...
Готовлюсь приветствовать...
>>> print(hi_adam)
None
    

К сожалению, декоратор «съел» значение, возвращаемое оригинальной функцией. Поскольку wrapper_do_twice() в явном виде не возвращает никакое значение, вызов в return_greeting("Адам") в конечном итоге вернул None.

Сделаем так, чтобы внутренняя функция декоратора возвращала значение декорированной функции. Поправим файл decorators.py:decorators.py

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

Проверим, как всё работает теперь:

>>> return_greeting("Адам")
Готовлюсь приветствовать...
Готовлюсь приветствовать...
'Привет, Адам!'
    

2.6. Интроспекция: «кто ты такой, в самом деле?»

Большое удобство в работе с 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 decorators.do_twice.<locals>.wrapper_do_twice(*args, **kwargs)>
>>> say_whee.__name__
'wrapper_do_twice'
>>> help(say_whee)
Help on function wrapper_do_twice in module decorators:

wrapper_do_twice(*args, **kwargs)
    

Как видим, в результате декорирования функция say_whee() запуталась в собственной идентичности. Теперь она сообщает, что является внутренней функцией wrapper_do_twice в модуле decorators. Хотя это технически верно, эта информация не очень полезна.

Чтобы исправить ситуацию, декоратор должен использовать… специальный декоратор @functools.wraps. Этот декоратор позволяет сохранить информацию об исходной функции. Снова уточним модуль decorators.py:

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 __main__.say_whee()>
>>> say_whee.__name__
'say_whee'
>>> help(say_whee)
Help on function say_whee in module __main__:

say_whee()
    

Гораздо лучше! Теперь у функции say_whee() не наступает амнезии после декорирования.

3. Несколько примеров из реального мира

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

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Что-то делаем до
        value = func(*args, **kwargs)
        # Что-то делаем после
        return value
    return wrapper_decorator
    

Этот блок кода является хорошим шаблоном для создания более сложных декораторов.

Примечание

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

3.1. Декоратор для тайминга кода

Начнем с создания декоратора @timer. Он будет измерять время выполнения функции и выводить результат в консоль:

import functools
import time

def timer(func):
    """Выводит время выполнения декорируемой функции"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter() 
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Функция {func.__name__!r} выполнена за {run_time:.4f} с")
        return value
    return wrapper_timer

@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])
    

Декоратор сохраняет текущее время в переменной start_time непосредственно перед запуском декорируемой функции. Это значение впоследствии вычитается из текущего значения end_time после выполнения функции. Полученная разность run_time передается в форматированную строку. Пара примеров:

>>> waste_some_time(1)
Функция 'waste_some_time' выполнена за 0.0010 с

>>> waste_some_time(999)
Функция 'waste_some_time' выполнена за 0.3260 с
    

Примечание

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

3.2. Отладочный декоратор

Следующий декоратор @debug будет выводить аргументы, с которыми вызвана функция, а также возвращаемое функцией значения:

import functools

def debug(func):
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Вызываем {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} возвращает {value!r}")         # 4
        return value
    return wrapper_debug
    

Отмеченные комментариями строки соответствуют следующим операциям:

  1. Создание списка позиционных аргументов: repr() используется для строкового представления каждого аргумента.
  2. Создание списка аргументов, передающихся по ключу: f-строка форматирует каждый элемент в формате key=value со спецификатором !r, соответствующим repr().
  3. Списки аргументов объединяются в общую подпись, элементы разделены запятыми.
  4. Возвращаемое значение выводится после исполняемой функции.

Давайте посмотрим, как декоратор работает на практике, применив его к простой функции с одним позиционным аргументов и одним аргументом, передаваемым по ключу:

 @debug
def make_greeting(name, age=None):
    if age is None:
        return f"Привет, {name}!"
    else:
        return f"Ого, {name}! Тебе уже {age}, как ты быстро растёшь!"
    

Тестируем:

>>> make_greeting("Бенджамин")
Вызываем make_greeting('Бенджамин')
'make_greeting' возвращает 'Привет, Бенджамин!'

>>> make_greeting("Ричард", age=112)
Вызываем make_greeting('Ричард', age=112)
'make_greeting' возвращает 'Ого, Ричард! Тебе уже 112, как ты быстро растёшь!'
'Ого, Ричард! Тебе уже 112, как ты быстро растёшь!'

>>> make_greeting(name="Доррисиль", age=116)
Вызываем make_greeting(name='Доррисиль', age=116)
'make_greeting' возвращает 'Ого, Доррисиль! Тебе уже 116, как ты быстро растёшь!'
'Ого, Доррисиль! Тебе уже 116, как ты быстро растёшь!'
    

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

import math
from decorators import debug

math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))
    

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

Всё, что нужно знать о декораторах Python

При вызове функции approximate_e() мы увидим @debug за работой:

>>> approximate_e(5)
Вызываем factorial(0)
'factorial' возвращает 1
Вызываем factorial(1)
'factorial' возвращает 1
Вызываем factorial(2)
'factorial' возвращает 2
Вызываем factorial(3)
'factorial' возвращает 6
Вызываем factorial(4)
'factorial' возвращает 24
2.708333333333333
    

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

3.3. Замедление кода

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

import time

def slow_down(func):
    """Ждёт 1 секунду, прежде чем вызвать переданную функцию"""
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down

@slow_down
def countdown(from_number):
    if from_number < 1:
        print("Поехали!")
    else:
        print(from_number)
        countdown(from_number - 1)
    

Чтобы увидеть результат действия декоратора, запустите пример:

 >>> countdown(3)
3
2
1
Поехали!
    

Декоратор @slow_down спит всегда лишь одну секунду. Позднее мы увидим, как передавать декоратору аргумент, чтобы контролировать его скорость.

3.4. Регистрация плагинов

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

import random
PLUGINS = dict()

def register(func):
    """Регистрирует функцию как плагин"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Привет, {name}!"

@register
def be_awesome(name):
    return f"Привет, {name}, классно быть вместе!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Используется {greeter!r}")
    return greeter_func(name)
    

В приведенном примере декоратор @register просто добавляет ссылку на декорируемую функцию в глобальный словарь PLUGINS. Никакой внутренней функции у декоратора нет, оригинальная функция возвращается немодифицированной, поэтому нет необходимости использовать @functools.wraps.

Функция randomly_greet() случайным образом выбирает, какую из зарегистрированных функций использовать для поздравления. Удобство состоит в том, что словарь PLUGINS уже содержит ссылку для каждой функции, к которой был применен декоратор @register:

>>> PLUGINS
{'say_hello': <function __main__.say_hello(name)>,
 'be_awesome': <function __main__.be_awesome(name)>}
>>> randomly_greet("Лео")
Используется 'say_hello'
'Привет, Лео!'
    

3.5. Залогинился ли пользователь?

Последний пример перед тем, как перейти к некоторым более изящным декораторам обычно используется при работе с веб-фреймворками. В этом примере мы используем Flask для настройки веб-страницы /secret – она должна быть видна только пользователям, вошедшим в систему:

from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)

def login_required(func):
    """Make sure user is logged in before proceeding"""
    @functools.wraps(func)
    def wrapper_login_required(*args, **kwargs):
        if g.user is None:
            return redirect(url_for("login", next=request.url))
        return func(*args, **kwargs)
    return wrapper_login_required

@app.route("/secret")
@login_required
def secret():
    ...
    

Примечание

Хотя приведенный пример дает представление о том, как добавить аутентификацию в веб-фреймворк, обычно нет нужды писать такие декораторы самостоятельно. В случае Flask используйте расширение Flask-Login. Оно решает вопросы безопасности и имеет больше возможностей для настройки.

4. Декораторы поинтереснее

До сих пор мы видели довольно простые декораторы – нам нужно было понять, как они работают. Вы можете передохнуть и попрактиковаться в применении декораторов, чтобы позднее вернуться к этому разделу, посвященному продвинутым концепциям.

На текущий момент наш файл decorators.py имеет следующее содержание:decorators.py

import functools
import time

def debug(func):
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Вызываем {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} возвращает {value!r}")         # 4
        return value
    return wrapper_debug


def timer(func):
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter() 
        value = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Функция {func.__name__!r} выполнена за {run_time:.4f} с")
        return value
    return wrapper_timer


def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice
    

4.1. Декорирование классов

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

Примечание

Некоторые широко используемые декораторы встроены в Python: @classmethod@staticmethod и @property. Первые два используются, чтобы определить методы внутри пространства имен классов, не связанные с конкретным экземпляром класса. Декоратор@property используется для настройки геттеров и сеттеров атрибутов класса.

Давайте определим класс, в котором декорируем некоторые из методов с помощью вышеописанных декораторов @debug и @timer:

from decorators import debug, timer

class TimeWaster:
    @debug
    def __init__(self, max_num):
        self.max_num = max_num

    @timer
    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([i**2 for i in range(self.max_num)])
    

Воспользуемся классом, чтобы увидеть действие декораторов:

>>> tw = TimeWaster(1000)
Вызываем __init__(<__main__.TimeWaster object at 0x7fd61850b710>, 1000)
'__init__' возвращает None

>>> tw.waste_time(999)
Функция 'waste_time' выполнена за 0.2181 с
    

Другой подход – декорировать классы целиком. Написание декоратора класса очень похоже на написание декоратора функции. Разница лишь в том, что декоратор в качестве аргумента получит класс, а не функцию. Однако когда мы применяем декораторы функций к классам, их эффект может оказаться не таким, как предполагалось. В следующем примере мы применили декоратор @timer к классу:

from decorators import timer

@timer
class TimeWaster:
    def __init__(self, max_num):
        self.max_num = max_num

    def waste_time(self, num_times):
        for _ in range(num_times):
            sum([i**2 for i in range(self.max_num)])
    

Декорирование класса не приведет к декорированию его методов. В результате @timer измерит только время создания экземпляра класса:

>>> tw = TimeWaster(1000)
Функция 'TimeWaster' выполнена за 0.0000 с

>>> tw.waste_time(999)
>>>
    

Позднее мы покажем примеры правильного декорирования классов.

4.2. Вложенные декораторы

К функции можно применить несколько декораторов, накладывая их действие друг на друга:

from decorators import debug, do_twice

@debug
@do_twice
def greet(name):
    print(f"Привет, {name}!")
    

В этом случае к функции будет применен сначала декоратор@do_twice, потом @debug:

>>> greet("Ева")
Вызываем greet('Ева')
Привет, Ева!
Привет, Ева!
'greet' возвращает None
    

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

from decorators import debug, do_twice

@do_twice
@debug
def greet(name):
    print(f"Привет, {name}!")
    
>>> greet("Ева")
Вызываем greet('Ева')
Привет, Ева!
'greet' возвращает None
Вызываем greet('Ева')
Привет, Ева!
'greet' возвращает None
    

4.3. Декораторы, принимающие аргументы

Иногда полезно передавать декораторам аргументы, чтобы управлять их поведением. Например, @do_twice может быть расширен до декоратора @repeat(num_times). Число повторений декорируемой функции можно было бы указать в качестве аргумента:

@repeat(num_times=4)
def greet(name):
    print(f"Привет, {name}!")
    
>>> greet("мир")
Привет, мир!
Привет, мир!
Привет, мир!
Привет, мир!
    

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

    def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat
    

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

Давайте проверим, работает ли, как задумано:

@repeat(num_times=4)
def greet(name):
    print(f"Привет, {name}!")
    
>>> greet("мир")
Привет, мир!
Привет, мир!
Привет, мир!
Привет, мир!
    

4.4. «И того, и другого, и можно без хлеба!»

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

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

    def name(_func=None, *, kw1=val1, kw2=val2, ...):
    def decorator_name(func):
        ...  # Создает и возвращает функцию-обёртку.

    if _func is None:
        return decorator_name
    else:
        return decorator_name(_func) 
    

Здесь аргумент _func действует как маркер, отмечающий, был ли декоратор вызван с аргументами или без них.

Если функция декоратора name будет вызвана без аргументов, декорируемая функция будет передана как _func. Если декоратор будет вызван с аргументами, тогда значение _func останется None, а передаваемые по ключу аргументы заменят значения по умолчанию. Символ * в списке аргументов означает, что следующие за ним аргументы не могут быть переданы как позиционные.

То есть в сравнении с предыдущей версией к декоратору добавилось условие if-else:

    def repeat(_func=None, *, num_times=2):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat

    if _func is None:
        return decorator_repeat
    else:
        return decorator_repeat(_func)
    
@repeat
def say_whee():
    print("Ура!")

@repeat(num_times=3)
def greet(name):
    print(f"Привет, {name}!")
    
>>> say_whee()
Ура!
Ура!

>>> greet('мир')
Привет, мир!
Привет, мир!
Привет, мир!
    

4.5. Декораторы, хранящие состояние

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

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

import functools

def count_calls(func):
    @functools.wraps(func)
    def wrapper_count_calls(*args, **kwargs):
        wrapper_count_calls.num_calls += 1
        print(f"{wrapper_count_calls.num_calls} вызов функции {func.__name__!r}")
        return func(*args, **kwargs)
    wrapper_count_calls.num_calls = 0
    return wrapper_count_calls

@count_calls
def say_whee():
    print("Ура!")
    
>>> say_whee()
1 вызов функции 'say_whee'
Ура!

>>> say_whee()
2 вызов функции 'say_whee'
Ура!

>>> say_whee.num_calls
2
    

4.6. Классы в качестве декораторов функций

Обычным способом хранения состояния является использование классов. Перепишем @count_calls из предыдущего раздела, используя в качестве декоратора класс.

Напомним, что синтаксис декоратора @my_decorator – это всего лишь более простой способ сказать func = my_decorator(func). Если my_decorator является классом, он должен принять func в качестве аргумента в методе __init__().

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

    class Counter:
    def __init__(self, start=0):
        self.count = start

    def __call__(self):
        self.count += 1
        print(f"Текущее значение счетчика – {self.count}")
    

Метод __call__() вызывается всякий раз, когда мы обращаемся к экземпляру класса:

>>> counter = Counter()
>>> counter()
Текущее значение счетчика – 1

>>> counter()
Текущее значение счетчика – 2

>>> counter.count
2
    

Таким, образом типичная реализация класса декоратора должна содержать __init__ () и __call__ ():

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"{self.num_calls} вызов функции {self.func.__name__!r}")
        return self.func(*args, **kwargs)

@CountCalls
def say_whee():
    print("Ура!")
    

Метод __init__()должен хранить ссылку на функцию и может выполнять любую другую необходимую инициализацию.

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

Обратите внимание, что в случае методов классов нужно использовать функцию functools.update_wrapper() вместо @functools.wraps.

Декоратор @CountCalls работает так же, как и в предыдущем разделе:

>>> say_whee()
1 вызов функции 'say_whee'
Ура!

>>> say_whee()
2 вызов функции 'say_whee'
Ура!

>>> say_whee.num_calls
2
    

5. Ещё несколько примеров из реального мира

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

5.1. Вновь замедляем код, но уже по-умному

Наша предыдущая реализация замедлителя кода @slow_down всегда «усыпляла» декорируемую функцию на одно и то же время. Давайте воспользуемся нашими знаниями о передачи в декоратор аргументов:

import functools
import time

def slow_down(_func=None, *, rate=1):
    """Усыпляет функцию перед вызовом на переданное количество секунд"""
    def decorator_slow_down(func):
        @functools.wraps(func)
        def wrapper_slow_down(*args, **kwargs):
            time.sleep(rate)
            return func(*args, **kwargs)
        return wrapper_slow_down

    if _func is None:
        return decorator_slow_down
    else:
        return decorator_slow_down(_func)
    

Проверим на примере функцииcountdown():

@slow_down(rate=2)
def countdown(from_number):
    if from_number < 1:
        print("Поехали!")
    else:
        print(from_number)
        countdown(from_number - 1)
    
>>> countdown(3)
3
2
1
Поехали!
    

5.2. Создание синглтонов

Синглтон – это класс с единственным экземпляром. В Python есть несколько часто используемых синглтонов, к примеру: NoneTrue и False. Тот факт, что None является синглтоном, позволяет использовать оператор is для сравнения объектов с None. Мы пользовались этим выше:

if _func is None:
    return decorator_name
else:
    return decorator_name(_func)
    

Оператор is возвращает True только для объектов, представляющих одну и ту же сущность.

Описанный ниже декоратор @singleton превращает класс в одноэлементный, сохраняя первый экземпляр класса в качестве атрибута. Последующие попытки создания экземпляра просто возвращают сохраненный экземпляр:

import functools

def singleton(cls):
    """Превращает класс в Singleton-класс с единственным экземпляром"""
    @functools.wraps(cls)
    def wrapper_singleton(*args, **kwargs):
        if not wrapper_singleton.instance:
            wrapper_singleton.instance = cls(*args, **kwargs)
        return wrapper_singleton.instance
    wrapper_singleton.instance = None
    return wrapper_singleton

@singleton
class TheOne:
    pass
    

Как видите, этот декоратор класса следует тому же шаблону, что и наши декораторы функций. Единственное отличие состоит в том, что мы используем cls вместо func в качестве имени параметра.

>>> first_one = TheOne()
>>> another_one = TheOne()

>>> id(first_one)
140242174053176

>>> id(another_one)
140242174053176

>>> first_one is another_one
True
    

first_one действительно представляет тот же экземпляр, что и another_one.

Примечание

Singleton-классы не так часто используются в Python, как в других языках. Эффект синглтона обычно лучше реализовать через глобальную переменную модуля.

5.3. Кэширование возвращаемых значений

Декораторы предоставляют прекрасный механизм для кэширования и мемоизации. В качестве примера давайте рассмотрим рекурсивное определение последовательности Фибоначчи:

from decorators import count_calls

@count_calls
def fibonacci(num):
    if num < 2:
        return num
    return fibonacci(num - 1) + fibonacci(num - 2)
    

Хотя реализация и выглядит просто, с производительностью дела обстоят плохо:

        >>> fibonacci(10)
<Множество вызовов из count_calls>
55

>>> fibonacci.num_calls
177
    

Чтобы рассчитать десятое число в последовательности Фибоначчи, в действительности достаточно лишь вычислить предыдущие числа этого ряда. Однако указанная реализация требует выполнения 177 вычислений. И ситуация быстро ухудшается: для 30-го числа потребуется 2.7 млн. операций. Это объясняется тем, что код каждый раз пересчитывает числа последовательности, уже известные из предыдущих этапов.

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

import functools
from decorators import count_calls

def cache(func):
    """Кэширует предыдущие вызовы функции"""
    @functools.wraps(func)
    def wrapper_cache(*args, **kwargs):
        cache_key = args + tuple(kwargs.items())
        if cache_key not in wrapper_cache.cache:
            wrapper_cache.cache[cache_key] = func(*args, **kwargs)
        return wrapper_cache.cache[cache_key]
    wrapper_cache.cache = dict()
    return wrapper_cache

@cache
@count_calls
def fibonacci(num):
    if num < 2:
        return num
    return fibonacci(num - 1) + fibonacci(num - 2)
    

Кэш работает как справочная таблица, поэтому теперь fibonacci() выполняет необходимые вычисления только один раз:

>>> fibonacci(10)
1 вызов функции 'fibonacci'
...
11 вызов функции 'fibonacci'
55

>>> fibonacci(8)
21
    

Заметьте, что при вызове fibonacci(8) не происходит никаких дополнительных расчетов – все необходимые значения уже найдены и сохранены при вычислении fibonacci(11).

Примечание

В стандартной библиотеке Python есть также декоратор для LRU-кэширования – @functools.lru_cache. Этот декоратор имеет больше возможностей, чем тот, что мы написали выше.

5.4. Добавление единиц измерения

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

    def set_unit(unit):
    """Регистрирует юнит для переданной функции"""
    def decorator_set_unit(func):
        func.unit = unit
        return func
    return decorator_set_unit
    

В следующем примере вычисляется объем цилиндра по известному радиусу и высоте, указанных в сантиметрах:

import math

@set_unit("см^3")
def volume(radius, height):
    return math.pi * radius**2 * height
    

Атрибут unit можно далее использовать по мере необходимости:

 >>> volume(3, 5)
141.3716694115407

>>> volume.unit
'см^3'
    

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

import math

def volume(radius, height) -> "cm^3":
    return math.pi * radius**2 * height
    

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

Примечание

Если вам нужно регулярно использовать единицы измерения, например, преобразовывать одни единицы в другие, обратите внимание на библиотеку pint: pip install Pint.

5.5. Валидация JSON

Рассмотрим последний пример практического применения декораторов. Взглянем на следующий обработчик маршрута Flask:

@app.route("/grade", methods=["POST"])
def update_grade():
    json_data = request.get_json()
    if "student_id" not in json_data:
        abort(400)
    # Update database
    return "success!"
    

Здесь мы гарантируем, что ключ student_id является частью запроса. Хотя эта проверка работает, на деле она не относится к самой функции. Кроме того, могут быть другие маршруты, которые используют ту же самую проверку. Итак, давайте абстрагируем всю стороннюю логику с помощью декоратора @validate_json:

from flask import Flask, request, abort
import functools
app = Flask(__name__)

def validate_json(*expected_args):
    def decorator_validate_json(func):
        @functools.wraps(func)
        def wrapper_validate_json(*args, **kwargs):
            json_object = request.get_json()
            for expected_arg in expected_args:
                if expected_arg not in json_object:
                    abort(400)
            return func(*args, **kwargs)
        return wrapper_validate_json
    return decorator_validate_json
    

В приведенном выше коде декоратор принимает в качестве аргумента список переменной длины. Каждый аргумент представляет собой ключ, используемый для проверки данных JSON. Функция-обертка проверяет, присутствует ли каждый ожидаемый ключ в данных JSON. Теперь обработчик маршрута может сосредоточиться на своей главной работе – обновлении оценок студентов:

@app.route("/grade", methods=["POST"])
@validate_json("student_id")
def update_grade():
    json_data = request.get_json()
    # Update database.
    return "Журнал оценкок успешно обновлен."
    

Заключение

Поздравляем, вы дошли до конца статьи! 🎖️

Итак, теперь вы знаете:

  • Как создавать декораторы функций и классов.
  • Как передавать в декораторы аргументы и возвращать из них значения.
  • Зачем в декораторах используется @functools.wraps.
  • Как использовать вложенные декораторы.
  • Как при помощи декораторов хранить состояния и кэшировать результаты функций.

В определении декораторов нет никакой магии. Обычно всё направлено на создание функции или класса, выступающих в качестве обёртки. Для передачи аргументов применяется обычная нотация *args и **kwargs. А использование знака @ представляет лишь синтаксический сахар, облегчающий вызов декораторов.

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

Для ещё более глубокого погружения в декораторы, посмотрите исторический документ PEP 318, а также вики-страницу, посвященную декораторам Python.

Сторонний модуль decorator также поможет вам в создании собственных декораторов. Его документация содержит ещё больше примеров использования декораторов.

Если вам понравилась эта статья, вот ещё три родственных материала по важным темам Python:

Заключение

Надеемся, эта статья помогла вам понять, какая «магия» лежит в основе работы декораторов.