Декораторы в Python: понятие, структура, примеры использования
Декораторы — один из паттернов программирования, очень активно применяемый в языке Python. Они позволяют динамически менять, расширять, дополнять логику и поведение функций, классов, методов. Такая задача в практике Python возникает чуть ли не ежедневно, а использование декораторов — удобно и наглядно. Не нужно менять исходный код, чтобы добавить новый функционал. Рассмотрим синтаксис, виды декораторов, случаи применения.
1. Области видимости и замыкания в Питоне
Прежде, чем перейти непосредственно к декораторам, важно понять специфику функций в языке Python, области видимости и замыкания.
Когда исполняется код, объявленные в нем переменные и объекты могут иметь сложную внутреннюю структуру. Питон понимает, как их обнаруживать, в каком порядке вызывать и возвращать. Существенный момент здесь — области видимости.
Например, вы в своем скрипте объявили переменную number, а где-то в другом месте этого файла вызывается функция, внутри которой есть своя переменная с таким же именем. Более того, никто не даст гарантию, что импортируемый модуль не содержит в себе аналогичное наименование. Такие ситуации не столь редки, как может показаться, следовательно, требуется ознакомиться с областями видимости в Python.
Всего их имеется 3 (от высшей к низшей):
— встроенная,
— на уровне модуля,
— локальная.
- Встроенная ( built-in ) область видимости отвечает за переменные и функции на уровне интерпретатора. В частности, в любом Python-проекте мы всегда можем обратиться к True, print, map и т.д. Эти объекты мы лично не объявляли, ниоткуда не импортировали, но спокойно используем. А все потому, что есть встроенная область видимости, которая уже «подтянула» данные сущности.
- Видимость на уровне модуля (или глобальная) связана с конкретным вызываемым скриптом. Здесь хранятся объекты, которые представлены в файле: переменные, функции, классы. После их объявления они готовы к использованию в программе.
- Локальная видимость представлена конкретными функциями, блоками кода. Их внутренние переменные не могут вызываться в других местах: произойдет ошибка.
Понимаем декораторы в Python’e, шаг за шагом. Шаг 1
Те же из вас, кто готов потратить немного времени, приглашаются прочесть длиииинный пост.
Функции в Python’e являются объектами
Для того, чтобы понять, как работают декораторы, в первую очередь следует осознать, что в Python’е функции — это тоже объекты.
Давайте посмотрим, что из этого следует:
def shout(word="да"): return word.capitalize() + "!" print shout() # выведет: 'Да!' # Так как функция - это объект, вы можете связать её с переменнной, # как и любой другой объект scream = shout # Заметьте, что мы не используем скобок: мы НЕ вызываем функцию "shout", # мы связываем её с переменной "scream". Это означает, что теперь мы # можем вызывать "shout" через "scream": print scream() # выведет: 'Да!' # Более того, это значит, что мы можем удалить "shout", и функция всё ещё # будет доступна через переменную "scream" del shout try: print shout() except NameError, e: print e #выведет: "name 'shout' is not defined" print scream() # выведет: 'Да!'
Запомним этот факт, скоро мы к нему вернёмся, но кроме того, стоит понимать, что функция в Python’e может быть определена… внутри другой функции!
def talk(): # Внутри определения функции "talk" мы можем определить другую. def whisper(word="да"): return word.lower() + ". " # . и сразу же её использовать! print whisper() # Теперь, КАЖДЫЙ РАЗ при вызове "talk", внутри неё определяется а затем # и вызывается функция "whisper". talk() # выведет: "да. " # Но вне функции "talk" НЕ существует никакой функции "whisper": try: print whisper() except NameError, e: print e #выведет : "name 'whisper' is not defined"
Ссылки на функции
Ну что, вы всё ещё здесь?:)
- могут быть связаны с переменной;
- могут быть определены одна внутри другой.
def getTalk(type="shout"): # Мы определяем функции прямо здесь def shout(word="да"): return word.capitalize()+"!" def whisper(word="да") : return word.lower()+". "; # Затем возвращаем необходимую if type == "shout": # Заметьте, что мы НЕ используем "()", нам нужно не вызвать функцию, # а вернуть объект функции return shout else: return whisper # Как использовать это непонятное нечто? # Возьмём функцию и свяжем её с переменной talk = getTalk() # Как мы можем видеть, "talk" теперь - объект "function": print talk # выведет: # Который можно вызывать, как и функцию, определённую "обычным образом": print talk() # Если нам захочется - можно вызвать её напрямую из возвращаемого значения: print getTalk("whisper")() # выведет: да.
Подождите, раз мы можем возвращать функцию, значит, мы можем и передавать её другой функции, как параметр:
def doSomethingBefore(func): print "Я делаю что-то ещё, перед тем как вызвать функцию, которую ты мне передал" print func() doSomethingBefore(scream) #выведет: # Я делаю что-то ещё, перед тем как вызвать функцию, которую ты мне передал # Да!
Ну что, теперь у нас есть все необходимые знания для того, чтобы понять, как работают декораторы.
Как вы могли догадаться, декораторы — это, по сути, просто своеобразные «обёртки», которые дают нам возможность делать что-либо до и после того, что сделает декорируемая функция, не изменяя её.
Создадим свой декоратор «вручную»
# Декоратор - это функция, ожидающая ДРУГУЮ функцию в качестве параметра def my_shiny_new_decorator(a_function_to_decorate): # Внутри себя декоратор определяет функцию-"обёртку". # Она будет (что бы вы думали. ) обёрнута вокруг декорируемой, # получая возможность исполнять произвольный код до и после неё. def the_wrapper_around_the_original_function(): # Поместим здесь код, который мы хотим запускать ДО вызова # оригинальной функции print "Я - код, который отработает до вызова функции" # ВЫЗОВЕМ саму декорируемую функцию a_function_to_decorate() # А здесь поместим код, который мы хотим запускать ПОСЛЕ вызова # оригинальной функции print "А я - код, срабатывающий после" # На данный момент функция "a_function_to_decorate" НЕ ВЫЗЫВАЛАСЬ НИ РАЗУ # Теперь, вернём функцию-обёртку, которая содержит в себе # декорируемую функцию, и код, который необходимо выполнить до и после. # Всё просто! return the_wrapper_around_the_original_function # Представим теперь, что у нас есть функция, которую мы не планируем больше трогать. def a_stand_alone_function(): print "Я – простая одинокая функция, ты ведь не посмеешь меня изменять. " a_stand_alone_function() # выведет: Я – простая одинокая функция, ты ведь не посмеешь меня изменять. # Однако, чтобы изменить её поведение, мы можем декорировать её, то есть # Просто передать декоратору, который обернет исходную функцию в любой код, # который нам потребуется, и вернёт новую, готовую к использованию функцию: a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function) a_stand_alone_function_decorated() #выведет: # Я - код, который отработает до вызова функции # Я простая одинокая функция, ты ведь не посмеешь меня изменять. # А я - код, срабатывающий после
Наверное, теперь мы бы хотели, чтобы каждый раз, во время вызова a_stand_alone_function, вместо неё вызывалась a_stand_alone_function_decorated. Нет ничего проще, просто перезапишем a_stand_alone_function функцией, которую нам вернул my_shiny_new_decorator:
a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function) a_stand_alone_function() #выведет: # Я - код, который отработает до вызова функции # Я простая одинокая функция, ты ведь не посмеешь меня изменять. # А я - код, срабатывающий после
Вы ведь уже догадались, что это ровно тоже самое, что делают @декораторы.:)
Разрушаем ореол таинственности вокруг декораторов
Вот так можно было записать предыдущий пример, используя синтаксис декораторов:
@my_shiny_new_decorator def another_stand_alone_function(): print "Оставь меня в покое" another_stand_alone_function() #выведет: # Я - код, который отработает до вызова функции # Оставь меня в покое # А я - код, срабатывающий после
Да, всё действительно так просто! decorator — просто синтаксический сахар для конструкций вида:
another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)
Декораторы — это просто pythonic-реализация паттерна проектирования «Декоратор». В Python включены некоторые классические паттерны проектирования, такие как рассматриваемые в этой статье декораторы, или привычные любому пайтонисту итераторы.
Конечно, можно вкладывать декораторы друг в друга, например так:
def bread(func): def wrapper(): print "------\>" func() print "<\______/>" return wrapper def ingredients(func): def wrapper(): print "#помидоры#" func() print "~салат~" return wrapper def sandwich(food="--ветчина--"): print food sandwich() #выведет: --ветчина-- sandwich = bread(ingredients(sandwich)) sandwich() #выведет: #------\> # #помидоры# # --ветчина-- # ~салат~ # <\______/>
И используя синтаксис декораторов:
@bread @ingredients def sandwich(food="--ветчина--"): print food sandwich() #выведет: #------\> # #помидоры# # --ветчина-- # ~салат~ # <\______/>
Следует помнить о том, что порядок декорирования ВАЖЕН:
@ingredients @bread def sandwich(food="--ветчина--"): print food sandwich() #выведет: # #помидоры# #------\> # --ветчина-- # <\______/># ~салат~
На этом моменте Вы можете счастливо уйти, с осознанием того, что вы поняли, что такое декораторы и с чем их едят.
Для тех же, кто хочет помучать ещё немного свой мозг, завтра будет допереведена вторая часть статьи, посвящённая продвинутому использованию декораторов.
- Шаг 1. Базовое понимание декораторов(вы сейчас прочитали эту статью)
- Шаг 2. Продвинутое использование декораторов
Как создать собственные Python-декораторы и правильно их использовать
Статья рассчитана на тех, кто владеет основами Python и хочет научиться создавать собственные декораторы для повышения качества кода.
Статья рассчитана на тех, кто владеет основами Python, знаком с декораторами и хочет научиться создавать собственные декораторы для повышения качества кода. Если вы забыли, что такое декораторы, — повторите тему по первым разделам статьи.
Михаил Залешин
К.ф.-м.н., Data Scientist, наставник и эксперт в Нетологии по курсам «Программист на JavaScript» и «Data Science: Рекомендательные системы»
Что такое декораторы в Python
Декоратор — конструкция языка Python для расширения возможностей функций и классов без изменения их кода.
Есть смартфон. Сделаем его устойчивым к падениям — наденем на него чехол. Чехол не изменяет прежние возможности смартфона, и добавляет к нему качество — ударопрочность. Чехол — декоратор смартфона.
Один и тот же чехол подходит для всех смартфонов нужной модели. Универсальность — важное свойство декораторов.
Анатомия декоратора в Python
Создадим декоратор @hello_decorator :
from functools import wraps def hello_decorator(f): @wraps(f) def wrapper(*args, **kwargs): print('Hello from decorator!') return f(*args, **kwargs) return wrapper
Декоратор в Python — функция, которая принимает функцию/класс и возвращает функцию/класс. В примере выше декоратор hello_decorator() принимает функцию f() , и возвращает функцию wrapper() .
- При объявлении функции wrapper() используем встроенный в Python декоратор @wraps . Этот декоратор копирует свойства __name__ , __doc__ и другие из функции f() в функцию wrapper() , чтобы при отладке программы все выглядело так, будто wrapper() и есть функция f() ;
- Аргументы функции wrapper() : *args и **kwargs . Аргумент *args собирает позиционные аргументы, а **kwargs — именованные. Например, в вызове: wrapper(1, ‘a’, x=5, y=None) значение args — кортеж (1, ‘a’) , а kwargs — словарь . Если позиционных аргументов при вызове функции нет, args — пустой, и если нет именованных аргументов, пустой — kwargs ;
- В первой строке тела функции wrapper() в консоль выводится «Hello from decorator!» — единственный «побочный эффект» декоратора. Далее вызывается декорируемая функция f() ;
- Функция f() внутри wrapper() принимает параметры *args и **kwargs . Операторы * и ** перед именами параметров в вызове функции имеют противоположный эффект случаю, когда их используют при объявлении аргументов. Например, если args=(1, ‘a’) и kwargs= , вызов f(*args, **kwargs) равносилен: f(1, ‘a’, x=5, y=None) . Комбинация «звездочных» аргументов и «звездочных» операторов позволяет универсально передать аргументы из функции wrapper() в f() ;
- В последней строке функции hello_decorator() возвращаем функцию-обертку wrapper() . Так мы указываем, что нужно подставить на место декорируемой функции f() . Вызывать функцию wrapper() не нужно — возвращаем саму функцию.
Применим декоратор @hello_decorator к функции sum2() :
@hello_decorator def sum2(a, b): return a + b
Функция sum2() принимает два аргумента и возвращает их сумму. Декорированный sum2() дополнительно выводит на консоль «Hello from decorator!».
Синтаксис @hello_decorator введен в Python для удобства, и равносилен такой записи:
def sum2(a, b): return a + b sum2 = hello_decorator(sum2)
Настраиваемый логгер-декоратор
Наметим логирующий декоратор @Logger для функций с учетом следующих пожеланий:
- Логи отправляются на консоль;
- Вложенность: если логируемая функция вызывает другую логируемую функцию, делаем при выводе на консоль отступ для последней;
- Уровни логов DEBUG , INFO , CRIT : при декорировании можно указать уровень лога функции, который работает в комбинации с настройкой детальности отображения логов. Если уровень лога функции выше или равен текущей детальности, отображаем лог, иначе — игнорируем;
- Для логирования вызова декорированной функции используем шаблон: LOG_LEVEL [TIMESTAMP] FUNC_NAME(ARGS, KWARGS) ;
- Логирование исключений: если функция выбрасывает исключение, логируем его с уровнем CRIT ;
- Внутренние логи: логгер должен работать и в режиме декоратора, и как функция для вывода в лог произвольных сообщений.
Начнем с примера использования. Так мы не перегружаем внимание внутренней сложностью и повышаем шансы создать удачный интерфейс модуля. На этом принципе основана разработка через тестирование — test-driven development (TTD).
Что хотим получить:
logger = Logger(verbosity=Logger.LogLevel.DEBUG) @logger() def load_data(url): from string import ascii_lowercase import random return random.choices(ascii_lowercase, k=3) @logger(log_level=Logger.LogLevel.DEBUG) def check_value(value): logger.log_msg( Logger.LogLevel.DEBUG, 'Doing some important stuff. ' ) return True @logger() def process_value(value): if check_value(value): return value.upper() return value @logger() def save_data(url, data): raise Exception('Could not save data :(') @logger() def main(): data = load_data('example.com') data = [*map(process_value, data)] save_data('example.com', data)
После вызова main() хотим увидеть в консоли:
⠀INFO [2022-09-29 17:14:26] main((), <>) INFO [2022-09-29 17:14:26] -> load_data(('example.com',), <>) INFO [2022-09-29 17:14:26] -> process_value(('z',), <>) DEBUG [2022-09-29 17:14:26] ---> check_value(('z',), <>) DEBUG [2022-09-29 17:14:26] -----> Doing some important stuff. INFO [2022-09-29 17:14:26] -> process_value(('u',), <>) DEBUG [2022-09-29 17:14:26] ---> check_value(('u',), <>) DEBUG [2022-09-29 17:14:26] -----> Doing some important stuff. INFO [2022-09-29 17:14:26] -> process_value(('f',), <>) DEBUG [2022-09-29 17:14:26] ---> check_value(('f',), <>) DEBUG [2022-09-29 17:14:26] -----> Doing some important stuff. INFO [2022-09-29 17:14:26] -> save_data(('example.com', ['Z', 'U', 'F']), <>) CRIT [2022-09-29 17:14:26] ---> Could not save data :(
Наблюдения по поводу декоратора @Logger :
- Logger — не функция, а класс. Экземпляры этого класса можно вызвать. Результат вызова — декоратор;
- У класса Logger есть вложенный класс-перечисление LogLevel с полями DEBUG , INFO , CRIT ;
- Детальность логирования verbosity определяется в конструкторе класса Logger , уровень логирования функции log_level задается при ее декорировании;
У класса Logger есть метод log_msg() , который можно использовать напрямую внутри функций.
Напишем скелет класса Logger :
from contextlib import contextmanager from functools import wraps from datetime import datetime from enum import Enum class Logger: class LogLevel(Enum): DEBUG = 0 INFO = DEBUG + 1 CRIT = INFO + 1 # Помогает принять решение о пропуске лога, # который имеет уровень ниже, чем установленная # детальность логирования def should_skip(self, verbosity): return self.value < verbosity.value # Определяет ширину отступа слева для каждого уровня лога PREFIX_OFFSET = 2 def __init__(self, verbosity=LogLevel.INFO): pass # Этот метод делает экземпляры класса вызываемыми # Используем его для декорирования # Параметр should_rethrow_exceptions определяет, нужно # ли подавлять исключения или передавать их на уровень выше def __call__(self, log_level=LogLevel.INFO, should_rethrow_exceptions=False): pass def log_msg(self, log_level, msg): pass def log_func(self, log_level, f, args, kwargs): pass # Контекстный менеджер для контроля глубины лога @contextmanager def _deeper(self): self._depth += 1 try: yield finally: self._depth -= 1
Конструктор класса Logger :
def __init__(self, verbosity=LogLevel.INFO): self._depth = 0 # для отслеживания глубины self.verbosity = verbosity
def log_msg(self, log_level, msg): # Пропускаем логи с уровнем ниже уровня детализации if log_level.should_skip(self.verbosity): return timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") prefix = f'5> [] ' prefix += '-' * (Logger.PREFIX_OFFSET * self._depth - 1) if self._depth > 0: prefix += '>' print(f' ')
Вспомогательный метод Logger.log_func() :
def log_func(self, log_level, f, args, kwargs): self.log_msg(log_level, f'(, )')
Ключевой метод-декоратор Logger.__call__() :
def __call__(self, log_level=LogLevel.INFO, should_rethrow_exceptions=False): # Декоратор должен принимать один аргумент — декорируемую # функцию # Если декоратору нужны параметры, используем замыкание с # помощью внешней функции def decorator(f): @wraps(f) def wrapper(*args, **kwargs): self.log_func(log_level, f, args, kwargs) try: # Используем менеджер контекста with self._deeper(): return f(*args, **kwargs) except Exception as e: # Логируем исключения if log_level.should_skip(self.verbosity): self.log_func(Logger.LogLevel.CRIT, f, args, kwargs) with self._deeper(): self.log_msg(Logger.LogLevel.CRIT, str(e)) if should_rethrow_exceptions: raise e return wrapper return decorator
Выводы
- Проектировать проще сверху-вниз: сначала решите, как хотите пользоваться модулем, а потом реализуйте его;
- Если декоратор требует централизованного контроля, используйте класс;
- Если декоратору нужны дополнительные параметры, используйте замыкание — оберните функцию-декоратор во внешнюю функцию (см. Logger.__call__() );
- Экземпляры классов можно использовать как функции за счет __call__() ;
- Менеджеры контекста удобны, когда нужно «подчищать» состояние.
Декораторы Python: пошаговое руководство
Понимание декораторов является важной вехой для любого программиста 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 >>> null_decorator(greet) >>> uppercase(greet) .wrapper at 0x10da02f28>
Как вы видели ранее, это необходимо для того, чтобы изменить поведение декорированной функции, когда она будет вызвана. Декоратор uppercase сам является функцией. И единственный способ повлиять на «будущее поведение» входной функции, которую он декорирует, — это заменить (или обернуть) входную функцию замыканием.
Вот почему uppercase определяет и возвращает другую функцию (замыкание), которую можно вызвать позднее, запустить исходную входную функцию и изменить ее результат.
Декораторы изменяют поведение вызываемого объекта с помощью обертки, поэтому вам не нужно постоянно изменять оригинал. Вызываемый объект не подвергается постоянным изменениям — ее поведение меняется только при декорировании.
Это позволяет «присоединять» к существующим функциям и классам повторно используемые модули, такие как журналирование и другие инструменты. Именно это делает декораторы такой мощной функцией в Python, которая часто используется в стандартной библиотеке и в пакетах сторонних разработчиков.
Небольшой перерыв
Кстати, если в этот момент вы почувствовали, что вам нужен небольшой перерыв на кофе — это совершенно нормально. На мой взгляд, замыкания и декораторы — одни из самых сложных для понимания концепций в Python. Не торопитесь и не стремитесь понять всё и сразу. Часто суть помогает понять запуск примеров в сессии интерпретатора один за другим.
Я знаю, что у вас всё получится
Применение нескольких декораторов к одной функции
Возможно, неудивительно, что к функции можно применить более одного декоратора. Это накапливает их эффекты, и именно это делает декораторы настолько полезными, как повторно используемые модули.
Вот пример. Следующие два декоратора оборачивают выводимую строку декорируемой функции в HTML-теги. Посмотрев на то, как вложены теги, можно увидеть, в каком порядке Python применяет несколько декораторов:
def strong(func): def wrapper(): return '' + func() + '' return wrapper def emphasis(func): def wrapper(): return '' + func() + '' return wrapper
Теперь давайте возьмем эти два декоратора и применим их к нашей функции greet одновременно. Для этого можно использовать обычный синтаксис @ и просто «уложить» несколько декораторов поверх одной функции:
@strong @emphasis def greet(): return 'Hello!'
Какой результат вы ожидаете увидеть, если запустите декорированную функцию? Будет ли декоратор @emphasis первым добавлять свой тег или @strong имеет приоритет? Вот что происходит, когда вы вызываете декорированную функцию:
>>> greet() 'Hello!'
Здесь хорошо видно, в каком порядке применялись декораторы: снизу вверх. Сначала входная функция была обернута декоратором @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 () ' f'with , ') original_result = func(*args, **kwargs) print(f'TRACE: () ' f'returned ') return original_result return wrapper
Декорировав функцию с помощью trace и вызвав ее, можно вывести аргументы, переданные декорированной функции, и ее возвращаемое значение. Это все еще в некоторой степени игрушечный пример, но в крайнем случае он станет отличным подспорьем для отладки:
@trace def say(name, line): return f': ' >>> 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".
- Блог компании OTUS
- Python
