Как работают декораторы python
Перейти к содержимому

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

  • автор:

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

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

Рассмотрим простейший пример:

# определение функции декоратора def select(input_func): def output_func(): # определяем функцию, которая будет выполняться вместо оригинальной print("*****************") # перед выводом оригинальной функции выводим всякую звездочки input_func() # вызов оригинальной функции print("*****************") # после вывода оригинальной функции выводим всякую звездочки return output_func # возвращаем новую функцию # определение оригинальной функции @select # применение декоратора select def hello(): print("Hello METANIT.COM") # вызов оригинальной функции hello()

Вначале определяется собственно функция декоратора, которая в данном случае называется select() . В качестве параметра декоратор получает функцию (в данном случае параметр input_func ), к которой этот декоратор будет применяться:

def select(input_func): def output_func(): # определяем функцию, которая будет выполняться вместо оригинальной print("*****************") # перед выводом оригинальной функции выводим всякую звездочки input_func() # вызов оригинальной функции print("*****************") # после вывода оригинальной функции выводим всякую звездочки return output_func # возвращаем новую функцию

Результатом декоратора в данном случае является локальная функция output_func , в которой вызывается входная функция input_func. Для простоты здесь перед и после вызыва input_func для красоты просто выводим набор символов «#».

Далее определяется стандартная функция, к которой применяется декоратор — в данном случае это функция hello , которая просто выводит на консоль некоторую строку:

@select # применение декоратора select def hello(): print("Hello METANIT.COM")

Для применения декоратора перед определением функции указывается символ @ , после которого идет имя декоратора. То есть в данном случае к функции hello() применяется декоратор select().

Далее вызываем обычную функцию:

hello()

Поскольку к этой функции применяется декоратор select, то в результате функциия hello передается в декоратор select() в качестве параметра input_func . И поскольку декоратор возвращает новую функцию — output_func, то фактически в данном случае будет выполняться именно эта функция output_func()

В итоге мы получим следующий консольный вывод:

***************** Hello METANIT.COM *****************

Получение параметров функции в декораторе

Декоратор может перехватывать передаваемые в функцию аргументы:

# определение функции декоратора def check(input_func): def output_func(*args): # через *args получаем значения параметров оригинальной функции input_func(*args) # вызов оригинальной функции return output_func # возвращаем новую функцию # определение оригинальной функции @check def print_person(name, age): print(f"Name: Age: ") # вызов оригинальной функции print_person("Tom", 38)

Здесь функция print_person() принимает два параметра: name (имя) и age (возраст). К этой функции применяется декоратор check()

В декораторе check возвращается локальная функция output_func() , которая принимает некоторый набор значений в виде параметра *args — это те значения, которые передаются в оригинальную функцию, к которой применяется декоратор. То есть в данном случае *args будет содержать значения параметров name и age.

def check(input_func): def output_func(*args): # через *args получаем значения параметров функции input_func

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

input_func(*args)

В итоге в данном получим следующий консольный вывод

Name: Tom Age: 38

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

# определение функции декоратора def check(input_func): def output_func(*args): name = args[0] age = args[1] # получаем значение второго параметра if age < 0: age = 1 # если возраст отрицательный, изменяем его значение на 1 input_func(name, age) # передаем функции значения для параметров return output_func # определение оригинальной функции @check def print_person(name, age): print(f"Name: Age: ") # вызов оригинальной функции print_person("Tom", 38) print_person("Bob", -5)

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

Name: Tom Age: 38 Name: Bob Age: 1

Получение результата функции

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

# определение функции декоратора def check(input_func): def output_func(*args): result = input_func(*args) # передаем функции значения для параметров if result < 0: result = 0 # если результат функции меньше нуля, то возвращаем 0 return result return output_func # определение оригинальной функции @check def sum(a, b): return a + b # вызов оригинальной функции result1 = sum(10, 20) print(result1) # 30 result2 = sum(10, -20) print(result2) # 0

Здесь определена функция sum() , которая возвращает сумму чисел. В декораторе check проверяем результат функции и для простоты, если он меньше нуля, то возвращаем 0.

Консольный вывод программы:

Декораторы в Python: понять и полюбить

Декораторы в Python — полезная вещь, но многие новички её не понимают и обходят стороной. Объясняем, что они из себя представляют и как работают.

Обложка поста Декораторы в 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 Hello: . pass . >>> type(Hello) >>> type(10)

Как вы заметили, функция hello_world принадлежит типу . Это означает, что она является объектом класса 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) Получена функция в качестве аргумента Hello world!

Из этих примеров должно стать понятно, насколько функции в 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() Оборачиваемая функция: Выполняем обёрнутую функцию. 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 секунд.   

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

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

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

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 исходной функции.

Заключение

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

Следите за новыми постами по любимым темам

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

Декораторы

Python 3 логотип

Декораторы в Python и примеры их практического использования.

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

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

Создадим свой декоратор "вручную":

Наверное, теперь мы бы хотели, чтобы каждый раз, во время вызова stand_alone_function, вместо неё вызывалась stand_alone_function_decorated. Для этого просто перезапишем stand_alone_function:

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

То есть, декораторы в python — это просто синтаксический сахар для конструкций вида:

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

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

Декорирование методов

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

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

        Python is cool, no argument here.   1 2 3         Любят ли Билл, Линус и Стив утконосов? Определенно!     ,) <> Мне 28 лет, а ты бы сколько дал?

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

А теперь попробуем написать декоратор, принимающий аргументы:

Теперь перепишем данный код с помощью декораторов:

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

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

Некоторые особенности работы с декораторами

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

Последняя проблема частично решена добавлением в модуле functools функции functools.wraps, копирующей всю информацию об оборачиваемой функции (её имя, из какого она модуля, её документацию и т.п.) в функцию-обёртку.

Забавным фактом является то, что functools.wraps тоже является декоратором.

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

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

                     wrapper 0.00011799999999997923 арозА упал ан алапу азор А      wrapper 0.00017800000000001148 !amanaP :lanac a ,noep a ,stah eros ,raj a,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a , . 

Для вставки кода на Python в комментарий заключайте его в теги

Декораторы 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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *