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

Декораторы в Python позволяют расширять и изменять поведение вызываемых объектов (функций, методов и классов) без постоянного изменения самого вызываемого объекта.

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

  • журналирование,

  • обеспечение контроля доступа и аутентификации,

  • инструментарий и функции управления временем,

  • ограничение скорости,

  • кэширование и многое другое.

Почему стоит изучать декораторы в Python?

Это справедливый вопрос. То, о чем я только что говорил, звучит довольно абстрактно, и может быть трудно понять, как декораторы могут помочь Python-разработчику в повседневной работе. Вот пример:

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

«Помнишь те отчеты TPS? Нам нужно добавить журналирование входных и выходных данных на каждом шаге генератора отчетов. Компании XYZ это нужно для аудита. Я сказал им, что мы успеем это сделать к среде».

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

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

Если вы знаете декораторы, вы спокойно улыбнетесь и скажете что-то вроде: «Окей, будет готово сегодня к 14:00».

Сразу после этого вы напечатаете код общего декоратора @audit_log (длиной всего около 10 строк) и добавите его перед каждым определением функции. Потом сделаете коммит и выпьете чашечку кофе.

Я здесь преувеличиваю, конечно. Но только немного. Декораторы действительно могут быть настолько мощными.

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

Но:

Понимание декораторов стоит того

Понимание того, как работают декораторы в Python, приносит огромную пользу.

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

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

Прежде чем мы погрузимся в тему, давайте освежим в памяти свойства функций первого класса в Python. Я написал руководство по ним на dbader.org, советую вам потратить несколько минут на его изучение. Вот наиболее важные выводы из «функций первого класса» для понимания декораторов:

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

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

Ну что, готовы к работе? Давайте приступим.

Основы декораторов Python

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

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

Итак, как выглядит реализация простого декоратора? В общих чертах декоратор — это вызываемый объект, который принимает на вход вызываемый объект и возвращает другой вызываемый объект.

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

def null_decorator(func):
    return func

Как видите, null_decorator является вызываемым объектом, он принимает на вход другой вызываемый объект и возвращает тот же самый входной объект, не изменяя его.

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

def greet():
    return 'Hello!'

greet = null_decorator(greet)

>>> greet()
'Hello!'

В этом примере я определил функцию greet, а затем сразу же декорировал ее, прогнав ее через функцию null_decorator. Я знаю, пока это не выглядит чем-то очень полезным (мы ведь специально разработали декоратор null, чтобы он был бесполезным, верно?), но через некоторое время это прояснит, как работает синтаксис декораторов в Python.

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

@null_decorator
def greet():
    return 'Hello!'

>>> greet()
'Hello!'

Разместить строки @null_decorator перед определением функции — это то же самое, что сначала определить функцию, а затем применить к ней декоратор. Использование синтаксиса @ — это просто синтаксический сахар и сокращение для этого часто используемого шаблона.

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

Пока все хорошо. Давайте посмотрим, как это делается.

Декораторы могут изменять поведение

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

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

def uppercase(func):
    def wrapper():
        original_result = func()
        modified_result = original_result.upper()
        return modified_result
    return wrapper

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

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

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

Пришло время увидеть декоратор uppercase в действии. Что произойдет, если декорировать им исходную функцию greet?

@uppercase
def greet():
    return 'Hello!'

>>> greet()
'HELLO!'

Надеюсь, это был тот результат, которого вы ожидали. Давайте рассмотрим подробнее, что здесь произошло. В отличие от null_decorator, декоратор uppercase возвращает другой объект функции, когда он декорирует функцию:

>>> greet
<function greet at 0x10e9f0950>

>>> null_decorator(greet)
<function greet at 0x10e9f0950>

>>> uppercase(greet)
<function uppercase.<locals>.wrapper at 0x10da02f28>

Как вы видели ранее, это необходимо для того, чтобы изменить поведение декорированной функции, когда она будет вызвана. Декоратор uppercase сам является функцией. И единственный способ повлиять на «будущее поведение» входной функции, которую он декорирует, — это заменить (или обернуть) входную функцию замыканием.

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

Декораторы изменяют поведение вызываемого объекта с помощью обертки, поэтому вам не нужно постоянно изменять оригинал. Вызываемый объект не подвергается постоянным изменениям — ее поведение меняется только при декорировании.

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

Небольшой перерыв

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

Я знаю, что у вас всё получится ????

Применение нескольких декораторов к одной функции

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

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

def strong(func):
    def wrapper():
        return '<strong>' + func() + '</strong>'
    return wrapper

def emphasis(func):
    def wrapper():
        return '<em>' + func() + '</em>'
    return wrapper

Теперь давайте возьмем эти два декоратора и применим их к нашей функции greet одновременно. Для этого можно использовать обычный синтаксис @ и просто «уложить» несколько декораторов поверх одной функции:

@strong
@emphasis
def greet():
    return 'Hello!'

Какой результат вы ожидаете увидеть, если запустите декорированную функцию? Будет ли декоратор @emphasis первым добавлять свой тег <em> или @strong  имеет приоритет? Вот что происходит, когда вы вызываете декорированную функцию:

>>> greet()
'<strong><em>Hello!</em></strong>'

Здесь хорошо видно, в каком порядке применялись декораторы: снизу вверх. Сначала входная функция была обернута декоратором @emphasis , а затем результирующая (декорированная) функция была снова обернута декоратором @strong.

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

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

decorated_greet = strong(emphasis(greet))

Здесь снова видно, что сначала применяется декоратор emphasis, а затем полученная обернутая функция снова оборачивается декоратором strong.

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

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

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

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

Здесь на помощь приходит функция Python *args и **kwargs для работы с переменным количеством аргументов. Декоратор proxy использует эту возможность:

def proxy(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

В этом декораторе есть два примечательных момента:

  • Он использует операторы * и ** в определении замыкания wrapper для сбора всех позиционных и ключевых аргументов и хранения их в переменных (args и kwargs).

  • Затем замыкание wrapper передает собранные аргументы исходной входной функции с помощью операторов «распаковки аргументов» * и **.

(Немного жаль, что значение операторов "звездочка" и "двойная звездочка" перегружено и меняется в зависимости от контекста, в котором они используются. Но я надеюсь, что вы поняли идею.)

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

def trace(func):
    def wrapper(*args, **kwargs):
        print(f'TRACE: calling {func.__name__}() '
              f'with {args}, {kwargs}')

        original_result = func(*args, **kwargs)

        print(f'TRACE: {func.__name__}() '
              f'returned {original_result!r}')

        return original_result
    return wrapper

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

@trace
def say(name, line):
    return f'{name}: {line}'

>>> say('Jane', 'Hello, World')
'TRACE: calling say() with ("Jane", "Hello, World"), {}'
'TRACE: say() returned "Jane: Hello, World"'
'Jane: Hello, World'

Кстати, об отладке — есть несколько моментов, которые следует иметь в виду при отладке декораторов.

Как писать «отлаживаемые» декораторы

Когда вы используете декоратор, на самом деле вы заменяете одну функцию другой. Одним из недостатков этого процесса является то, что он «скрывает» некоторые метаданные, прикрепленные к исходной (недекорированной) функции.

Например, оригинальное имя функции, ее документационная строка (docstring) и список параметров скрываются замыканием:

def greet():
    """Return a friendly greeting."""
    return 'Hello!'

decorated_greet = uppercase(greet)

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

>>> greet.__name__
'greet'
>>> greet.__doc__
'Return a friendly greeting.'

>>> decorated_greet.__name__
'wrapper'
>>> decorated_greet.__doc__
None

Это делает отладку и работу с интерпретатором Python неудобной и сложной. К счастью, для этого есть быстрое решение: декоратор functools.wraps, включенный в стандартную библиотеку Python.

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

import functools

def uppercase(func):
    @functools.wraps(func)
    def wrapper():
        return func().upper()
    return wrapper

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

@uppercase
def greet():
    """Return a friendly greeting."""
    return 'Hello!'

>>> greet.__name__
'greet'
>>> greet.__doc__
'Return a friendly greeting.'

Я бы рекомендовал использовать functools.wraps во всех декораторах, которые вы пишете сами. Это не займет много времени и избавит вас (и других) от головной боли при отладке в будущем.

Основные выводы

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

  • Синтаксис @ — это просто сокращение для вызова декоратора на входной функции. Несколько декораторов на одной функции применяются снизу вверх (наложение декораторов).

  • В качестве лучшей практики отладки используйте хелпер functools.wraps в своих декораторах, чтобы перенести метаданные из недекорированной вызываемой функции в декорированную.


В заключение статьи приглашаем всех желающих на открытое занятие «Генерация тестовых данных с использованием библиотеки Faker». Используя библиотеку Faker и датаклассы, создадим JSON-объекты различных уровней вложенности и сложности. Сможем сгенерировать персональные данные тестовых пользователей, IP-адреса, информацию о файлах и другие параметры и написать красивый код с минимумом трудозатрат. Записаться на урок можно на странице курса "Python QA Engineer".

Комментарии (2)


  1. KhodeN
    07.04.2023 13:04

    А подскажите, плиз, как не сломать типы у метода/функции при использовании декоратора? Допустим, декоратор не меняет сигнатуру.


  1. rSedoy
    07.04.2023 13:04
    +5

    1001 статья про декораторы на Python, ничего нового, опять про одно и то же, остановитесь уже.