Путь к пониманию декораторов в Python
Прим. Wunder Fund: В этой статье разбираемся, что такое декораторы в Python, зачем они нужны, и в чем их прикол. Статья будет полезна начинающим разработчикам.
Материал рассчитан на начинающих программистов, которые хотят разобраться с тем, что такое декораторы, и с тем, как применять их в своих проектах.
Что такое декораторы?
Декораторы — это обёртки вокруг Python-функций (или классов), которые изменяют работу того, к чему они применяются. Декоратор максимально абстрагирует собственные механизмы. А синтаксические конструкции, используемые при применении декораторов, устроены так, чтобы они как можно меньше влияли бы на код декорируемых сущностей. Разработчики могут создавать код для решения своих специфических задач так, как привыкли, а декораторы могут использовать исключительно для расширения функционала своих разработок. Всё это — очень общие утверждения, поэтому перейдём к примерам.
В Python декораторы используются, в основном, для декорирования функций (или, соответственно, методов). Возможно, одним из самых распространённых декораторов является декоратор @property :
class Rectangle: def __init__(self, a, b): self.a = a self.b = b @property def area(self): return self.a * self.b rect = Rectangle(5, 6) print(rect.area) # 30
В последней строке кода, мы можем обратиться к члену area экземпляра класса Rectangle как к атрибуту. То есть — нам не нужно вызывать метод area . Вместо этого при обращении к area как к атрибуту (то есть — без использования скобок, () ), соответствующий метод вызывается неявным образом. Это возможно благодаря декоратору @property .
Как работают декораторы?
Размещение конструкции @property перед определением функции равносильно использованию конструкции вида area = property(area) . Другими словами, property — это функция, которая принимает другую функцию в качестве аргумента и возвращает ещё одну функцию. Именно этим и занимаются декораторы.
В результате оказывается, что декоратор меняет поведение декорируемой функции.
Декораторы функций
Декоратор retry
Мы дали довольно-таки размытое определение декораторов. Для того чтобы разобраться с тем, как именно они работают, займёмся написанием собственных декораторов.
Предположим, имеется функция, которую мы хотим запустить повторно в том случае, если при её первом запуске произойдёт сбой. То есть — нам нужна функция (декоратор, имя которого, retry , можно перевести как «повтор»), которая вызывает нашу функцию один или два раза (это зависит от того, возникнет ли ошибка при первом вызове функции).
В соответствии с ранее данным определением — мы можем сделать код нашего простого декоратора таким:
def retry(func): def _wrapper(*args, **kwargs): try: func(*args, **kwargs) except: time.sleep(1) func(*args, **kwargs) return _wrapper @retry def might_fail(): print("might_fail") raise Exception might_fail()
Наш декоратор носит имя retry . Он принимает в виде аргумента ( func ) любую функцию. Внутри декоратора определяется новая функция ( _wrapper ), после чего осуществляется возврат этой функции. Тому, кто впервые видит код декоратора, может показаться непривычным объявление одной функции внутри другой функции. Но это — совершенно корректная синтаксическая конструкция, следствием применения которой является тот полезный для нас факт, что функция _wrapper видна лишь внутри пространства имён декоратора retry .
Обратите внимание на то, что в этом примере мы декорируем функцию might_fail() с использованием конструкции, которая выглядит @retry . После имени декоратора нет круглых скобок. В результате получается, что когда мы, как обычно, вызываем функцию might_fail() , на самом деле, вызывается декоратор retry , которому передаётся, в виде первого аргумента, целевая функция ( might_fail ).
Получается, что, в общей сложности, тут мы поработали с тремя функциями:
В некоторых случаях нужно, чтобы декораторы принимали бы дополнительные аргументы. Например, нам может понадобиться, чтобы декоратор retry принимал бы число, задающее количество попыток запуска декорируемой функции. Но декоратор обязательно должен принимать декорируемую функцию в качестве первого аргумента. Не будем забывать и о том, что нам не надо вызывать декоратор при декорировании функции. То есть — о том, что перед определением функции мы используем конструкцию @retry , а не @retry() . Подытожим:
- Декоратор — это всего лишь функция (которая, в качестве аргумента, принимает другую функцию).
- Декораторами пользуются, помещая их имя со знаком @ перед определением функции, а не вызывая их.
Следовательно, мы можем ввести в код четвёртую функцию, которая принимает параметр, с помощью которого мы хотим настраивать поведение декоратора, и возвращает функцию, которая и является декоратором (то есть — принимает в качестве аргумента другую функцию).
Попробуем такую конструкцию:
def retry(max_retries): def retry_decorator(func): def _wrapper(*args, **kwargs): for _ in range(max_retries): try: func(*args, **kwargs) except: time.sleep(1) return _wrapper return retry_decorator @retry(2) def might_fail(): print("might_fail") raise Exception might_fail()
Разберём этот код:
- На первом уровне тут имеется функция retry .
- Функция retry принимает произвольный аргумент (в нашем случае — max_retries ) и возвращает другую функцию — retry_decorator .
- Функция retry_decorator — это и есть реальный декоратор.
- Функция _wrapper работает так же, как и прежде (только теперь она руководствуется сведениями о максимальном количестве перезапусков декорированной функции).
О коде нового декоратора мне больше сказать нечего. Теперь поговорим об его использовании:
- Функция might_fail теперь декорируется с помощью вызова функции вида @retry(2) .
- Вызов retry(2) приводит к тому, что вызывается функция retry , которая и возвращает реальный декоратор.
- В итоге функция might_fail декорируется с помощью retry_decorator , так как именно эта функция представляет собой результат вызова функции retry(2) .
Декоратор timer
Напишем ещё один полезный декоратор — timer («таймер»). Он будет измерять время выполнения декорированной с его помощью функции:
import functools import time def timer(func): @functools.wraps(func) def _wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) runtime = time.perf_counter() - start print(f" took secs") return result return _wrapper @timer def complex_calculation(): """Some complex calculation.""" time.sleep(0.5) return 42 print(complex_calculation())
Вот результаты выполнения этого кода:
complex_calculation took 0.5041 secs 42
Видно, что декоратор timer выполняет какой-то код до и после вызова декорируемой функции. В остальном же он работает точно так же, как декоратор, рассмотренный в предыдущем разделе. Но при его написании мы воспользовались и кое-чем новым.
Декоратор functools.wraps
Анализируя вышеприведённый код, вы могли заметить, что сама функция _wrapper декорирована с помощью @functools.wraps . Но это никоим образом не меняет логику или функционал декоратора timer . При этом разработчик может принять решение о целесообразности использования functools.wraps .
Но, так как декоратор @timer может быть представлен как complex_calculation = timer(complex_calculation) , он обязательно изменяет функцию complex_calculation . В частности, он меняет некоторые из отражённых магических методов:
- __module__
- __name__
- __qualname__
- __doc__
- __annotations__
При использовании декоратора @functools.wraps эти атрибуты возвращаются к их исходному состоянию.
Вот что получится без @functools.wraps :
print(complex_calculation.__module__) # __main__ print(complex_calculation.__name__) # wrapper_timer print(complex_calculation.__qualname__) # timer..wrapper_timer print(complex_calculation.__doc__) # None print(complex_calculation.__annotations__) # <>
А использование @functools.wraps даёт нам следующее:
print(complex_calculation.__module__) # __main__# print(complex_calculation.__name__) # complex_calculation print(complex_calculation.__qualname__) # complex_calculation print(complex_calculation.__doc__) # Some complex calculation. print(complex_calculation.__annotations__) # <>
Декораторы классов
До сих пор мы обсуждали декораторы для функций. Но декорировать можно и классы.
Возьмём декоратор timer из предыдущего примера. Он отлично подходит и в качестве обёртки для класса:
@timer class MyClass: def complex_calculation(self): time.sleep(1) return 42 my_obj = MyClass() my_obj.complex_calculation()
Вот что нам это даст:
Finished 'MyClass' in 0.0000 secs
Видно, что здесь нет сведений о времени выполнения метода complex_calculation . Вспомним о том, что конструкция, начинающаяся с @ — это всего лишь эквивалент MyClass = timer(MyClass) . То есть — декоратор вызывается только когда «вызывают» класс. «Вызов» класса — это создание его экземпляра. Получается, что timer вызывается лишь при выполнении строки кода my_obj = MyClass() .
При декорировании класса методы этого класса не подвергаются автоматическому декорированию. Проще говоря — использование обычного декоратора для декорирования обычного класса приводит лишь к декорированию конструктора (метод __init__ ) этого класса.
Но можно поменять поведение всего класса, воспользовавшись другой формой конструктора. Правда, прежде чем об этом говорить, давайте поинтересуемся тем, может ли декоратор работать несколько иначе — то есть можно ли декорировать функцию с помощью класса. Оказывается — это возможно:
class MyDecorator: def __init__(self, function): self.function = function self.counter = 0 def __call__(self, *args, **kwargs): self.function(*args, **kwargs) self.counter+=1 print(f"Called times") @MyDecorator def some_function(): return 42 some_function() some_function() some_function()
Вот что получится:
Called 1 times Called 2 times Called 3 times
В ходе работы этого кода происходит следующее:
- Функция __init__ вызывается при декорировании some_function . Тут, снова, не забываем о том, что использование декоратора — это аналог конструкции some_function = MyDecorator(some_function) .
- Функция __call__ вызывается при использовании экземпляра класса, например — при вызове функции. Функция some_function — это теперь экземпляр класса MyDecorator , но использовать мы её при этом планируем как функцию. За это отвечает магический метод __call__ , в имени которого используются два символа подчёркивания.
Декорирование класса в Python, с другой стороны, работает путём изменения класса извне (то есть — из декоратора).
Взгляните на этот код:
def add_calc(target): def calc(self): return 42 target.calc = calc return target @add_calc class MyClass: def __init__(): print("MyClass __init__") my_obj = MyClass() print(my_obj.calc())
Вот что он выдаст:
MyClass __init__ 42
Если вспомнить определение декоратора, то всё, что тут происходит, следует уже знакомой нам логике:
- Вызов my_obj = MyClass() инициирует последовательность действий, которая начинается с вызова декоратора.
- Декоратор add_calc дополняет класс методом calc .
- В итоге создаётся экземпляр класса с использованием конструктора.
Декораторы можно использовать для изменения классов по принципам, соответствующим механизмам наследования. Хорошо это для некоего проекта, или плохо — сильно зависит от архитектуры конкретного Python-проекта. Декоратор dataclass из стандартной библиотеки — это отличный пример целесообразности применения декоратора, а не наследования. Скоро мы остановимся на этом подробнее.
Использование декораторов
Декораторы в стандартной библиотеке Python
В следующих разделах мы познакомимся с несколькими наиболее популярными и наиболее широко используемыми декораторами, которые включены в состав стандартной библиотеки Python.
Декоратор property
Как уже было сказано, @property — это, скорее всего, один из самых популярных Python-декораторов. Его цель заключается в том, чтобы обеспечить доступ к результатам вызова метода класса в такой форме, как будто этот метод является атрибутом. Конечно, существует и альтернатива @property , что позволяет, при выполнении операции присваивания значения, самостоятельно выполнять вызов метода.
class MyClass: def __init__(self, x): self.x = x @property def x_doubled(self): return self.x * 2 @x_doubled.setter def x_doubled(self, x_doubled): self.x = x_doubled // 2 my_object = MyClass(5) print(my_object.x_doubled) # 10 print(my_object.x) # 5 my_object.x_doubled = 100 # print(my_object.x_doubled) # 100 print(my_object.x) # 50
Декоратор staticmethod
Ещё один широко известный декоратор — это @staticmethod . Он используется в ситуациях, когда надо вызвать функцию, объявленную в классе, не создавая при этом экземпляр данного класса:
class C: @staticmethod def the_static_method(arg1, arg2): return 42 print(C.the_static_method())
Декоратор functools.cache
При работе с функциями, выполняющими сложные вычисления, может понадобиться кешировать результаты их работы.
Например, можно соорудить нечто вроде такого кода:
_cached_result = None def complex_calculations(): if _cached_result is None: _cached_result = something_complex() return _cached_result
Использование глобальной переменной, вроде _cached_result , проверка её на None , запись в эту переменную некоего значения в том случае, если она не равна None — всё это — повторяющиеся задачи. А значит — перед нами идеальная ситуация для применения декораторов. Но самостоятельно писать такой декоратор нам не придётся — в стандартной библиотеке Python есть именно то, что нужно для решения этой задачи — декоратор cache :
from functools import cache @cache def complex_calculations(): return something_complex()
Теперь, при попытке вызова complex_calculations() , Python, перед вызовом функции something_complex , проверяет, имеется ли кешированный результат её работы. Если результат её вызова имеется в кеше — something_complex не придётся вызывать дважды.
Декоратор dataclass
Там, где мы говорили о декораторах классов, мы видели, что декораторы можно использовать для модификации поведения классов, применяя ту же схему, которая используется для изменении поведения классов при наследовании.
Модуль стандартной библиотеки dataclasses — это хороший пример механизма, применение которого в определённых ситуациях предпочтительнее применения механизмов наследования. Сначала давайте посмотрим на всё это в действии:
from dataclasses import dataclass @dataclass class InventoryItem: name: str unit_price: float quantity: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity item = InventoryItem(name="", unit_price=12, quantity=100) print(item.total_cost()) # 1200
На первый взгляд кажется, что декоратор @dataclass просто снимает с нас нагрузку по написанию конструктора класса, позволяя избежать ручного написания кода, подобного следующему:
. def __init__(self, name, unit_price, quantity): self.name = name self.unit_price = unit_price self.quantity = quantity .
Но не всё так просто. Предположим, решено оснастить Python-проект REST-API, при этом встанет необходимость преобразовывать Python-объекты в JSON-строки.
Существует пакет dataclasses-json (не входящий в состав стандартной библиотеки), который позволяет декорировать классы данных и даёт возможность превращать объекты в их JSON-представление и выполнять обратное преобразование, производить сериализацию и десериализацию объектов.
Вот как это выглядит:
from dataclasses import dataclass from dataclasses_json import dataclass_json @dataclass_json @dataclass class InventoryItem: name: str unit_price: float quantity: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity item = InventoryItem(name="", unit_price=12, quantity=100) print(item.to_dict()) #
Разбирая этот код, можно сделать два наблюдения:
- Декораторы могут быть вложены друг в друга. При этом важен порядок их появления в коде.
- Декоратор @dataclass_json добавляет к классу метод to_dict .
Конечно, можно написать миксин (mixin, подмешанный класс), ответственный за решение всех сложных задач, связанных с типобезопасной реализацией метода to_dict . Потом можно сделать наш класс InventoryItem наследником этого класса.
В предыдущем примере, однако, декоратор оснащает класс лишь техническим функционалом (в противоположность расширению возможностей класса с учётом конкретной задачи). В результате можно отключать и подключать этот декоратор, не меняя поведения основной программы. Этот подход позволяет сохранить нашу «естественную» иерархию классов, код проекта не придётся подвергать изменениям. Декоратор dataclasses-json можно добавить в проект, не переписывая при этом тела существующих методов.
В подобном случае модификация поведения класса с помощью декораторов выглядит гораздо более элегантным решением (за счёт его лучшей модульности), чем применение наследования или миксинов.
О, а приходите к нам работать?
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Что такое декоратор
Декораторы в 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, однако новичкам они могут показаться непонятными. Возможно, вы уже встречались с ними, например, при работе с 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 секунд.
Как вы видите, аргументы декорируемой функции передаются функции-обёртке, после чего с ними можно делать что угодно. Можно изменять аргументы и затем передавать их декорируемой функции, а можно оставить их как есть или вовсе забыть про них и передать что-нибудь совсем другое. То же касается возвращаемого из декорируемой функции значения, с ним тоже можно делать что угодно.
Звёздный Python: где и как используются * и **
Декораторы с аргументами
Мы также можем создавать декораторы, которые принимают аргументы. Посмотрим на пример:
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
На собеседованиях только и разговоров, что о декораторах. Разбираемся на пальцах, что это.
Цокто Жигмытов
Кандидат философских наук, специалист по математическому моделированию. Пишет про Data Science, AI и программирование на Python.
Итак, вы на собеседовании на вакансию джуна-пайтониста. Всё идёт хорошо: вы объяснили про кортежи и списки, про принципы ООП и структуры данных, даже решили небольшую задачку, и вдруг:
— Расскажите, пожалуйста, про декораторы в Python.
Простой ответ
В большинстве случаев будет достаточно сказать своими словами, что такое декоратор, и написать простейший код.
Вслух:
— Декоратор, если в двух словах, это функция, которая добавляет новую функциональность к другой функции без изменения её кода. Он как бы оборачивает, декорирует функцию, тем самым расширяя её возможности.
Пример кода:
Функция say_hi (пример отсюда), которую мы «обернём» в декоратор, возвращает строку «всем привет». Обратите внимание: не печатает, а возвращает.
def say_hi(): return 'всем привет'
А наш декоратор превратит символы этой строки из строчных в прописные. Этой возможности у функции say_hi раньше не было, а теперь будет.
def uppercase_decorator(function): def wrapper(): func = function() make_uppercase = func.upper() return make_uppercase return wrapper
Что здесь происходит — разбираем код построчно:
- В первой строке мы указываем имя декоратора и то, что он принимает function в качестве своей переменной.
- Вторая строка — это объявление функции-обёртки wrapper (). Тело обёртки в блоке состоит из трёх строк ниже. Оно как раз и описывает, что именно мы будем делать с функцией, ранее принятой декоратором.
- Третья строка: записываем входящую переменную function () в локальную переменную func. Здесь «локальная» означает, что она действует только в рамках функции wrapper ().
- Четвёртая строка: мы применяем к func строковый метод upper и записываем результат в другую локальную переменную make_uppercase.
Больше локальных переменных богу локальных переменных! Конечно, всё это ради понятности и читаемости.
- Пятая строка: функция wrapper () возвращает переменную make_uppercase, то есть строку от function (), но уже прописными буквами.
- Последняя строка: декоратор возвращает нам уже саму функцию wrapper, точнее, результат её работы над функцией function.
Как это запустить? Пишем символ @, за ним название декоратора, а объявление функции say_hi переносим на строку под ней:
@uppercase_decorator def say_hi(): return 'всем привет'
Печатаем вывод декорированной функции:
print(say_hi())
Получаем на выходе:
Другие примеры
Если хочется немного дополнить, вот ещё примеры декораторов.
Логирование простых функций
def logging(func): def log_function_called(): print(f'Вызвана ') func() return log_function_called @logging def my_name(): print('Крис') @logging def friends_name(): print('Наруто') my_name() friends_name()
Что здесь происходит:
Декоратор принимает в качестве переменной функцию func. Затем функция-обёртка log_function_called печатает «Вызвана func» и запускает её на выполнение. Функции my_name и friends_name печатают имена Крис и Наруто, а в обёрнутом виде декоратор предваряет их выполнение сообщением об их вызове.
Измерение времени выполнения GET-запроса к серверу
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://skillbox.ru') print(webpage) fetch_webpage()
Что здесь происходит:
В объявлении декоратора benchmark мы импортируем библиотеку time. В функции-обёртке wrapper засекаем время в переменную start, затем выполняем полученную декоратором функцию, а после этого фиксируем время в переменную end. Далее печатаем время выполнения, просто отняв от величины end величину start прямо внутри команды print.
Сама функция fetch_webpage делает простой GET-запрос на наш сайт. Для этого она импортирует популярную библиотеку requests, записывает в переменную webpage ответ сервера Skillbox на запрос и печатает эту переменную.
В обёрнутом виде декоратор «запускает секундомер», функция fetch_webpage делает запрос и печатает ответ, после чего декоратор «выключает секундомер» и печатает время, которое функция потратила на всю эту работу.
Ответы посложнее
Если хотите произвести впечатление, то можно добавить, что:
- Декоратор — это паттерн проектирования (design pattern) в Python, а также функция второго уровня, то есть принимающая другие функции в качестве переменных и возвращающая их.
- И в сам декоратор, и в функцию-обёртку можно передать и позиционные, и именованные аргументы — args и kwargs соответственно.
- Декораторы работают не только с функциями, но и с классами и методами.
Конечно, на собеседовании надо будет пояснить все эти пункты и проиллюстрировать их кодом. На курсе Профессия Python-разработчик вы изучите не только декораторы, но и всё необходимое для того, чтобы с блеском проходить любые собеседования.
Читайте также:
- Как начать программировать на Python
- Ох уж эти детки: 10 вундеркиндов из мира IT и не только
- Как установить и запустить Python на Windows, Linux и macOS