Что такое декоратор: Декораторы в Python: понять и полюбить
Путь к пониманию декораторов в 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
видна лишь внутри пространства имён декоратора
.
Обратите внимание на то, что в этом примере мы декорируем функцию might_fail()
с использованием конструкции, которая выглядит @retry
. После имени декоратора нет круглых скобок. В результате получается, что когда мы, как обычно, вызываем функцию might_fail()
, на самом деле, вызывается декоратор retry
, которому передаётся, в виде первого аргумента, целевая функция (might_fail
).
Получается, что, в общей сложности, тут мы поработали с тремя функциями:
retry
_wrapper
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"{func.__name__} took {runtime:.4f} 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.<locals>.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 {self.counter} 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()) # {'name': '', 'unit_price': 12, 'quantity': 100}
Разбирая этот код, можно сделать два наблюдения:
Декораторы могут быть вложены друг в друга. При этом важен порядок их появления в коде.
Декоратор
@dataclass_json
добавляет к классу методto_dict
.
Конечно, можно написать миксин (mixin, подмешанный класс), ответственный за решение всех сложных задач, связанных с типобезопасной реализацией метода to_dict
. Потом можно сделать наш класс InventoryItem
наследником этого класса.
В предыдущем примере, однако, декоратор оснащает класс лишь техническим функционалом (в противоположность расширению возможностей класса с учётом конкретной задачи). В результате можно отключать и подключать этот декоратор, не меняя поведения основной программы. Этот подход позволяет сохранить нашу «естественную» иерархию классов, код проекта не придётся подвергать изменениям. Декоратор dataclasses-json
можно добавить в проект, не переписывая при этом тела существующих методов.
В подобном случае модификация поведения класса с помощью декораторов выглядит гораздо более элегантным решением (за счёт его лучшей модульности), чем применение наследования или миксинов.
О, а приходите к нам работать? 😏Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Присоединяйтесь к нашей команде.
Python | Декораторы
Последнее обновление: 08.10.2022
Декораторы в 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: {name} Age: {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: {name} Age: {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.
Консольный вывод программы:
НазадСодержаниеВперед
7. Декораторы — Советы по Python 0.1 документация
Декораторы — важная часть Python. Простыми словами: они функции, которые изменяют функциональность других функций. Они помогают чтобы сделать наш код короче и более Pythonic. Большинство новичков не знаю, где их использовать, поэтому я собираюсь поделиться некоторыми областями, где декораторы могут сделать ваш код более лаконичным.
Во-первых, давайте обсудим, как написать собственный декоратор.
Возможно, это одна из самых сложных концепций для понимания. Мы возьмем это шаг за шагом, чтобы вы могли полностью понять это.
7.1. Все в Python является объектом:
Прежде всего, давайте разберемся с функциями в Python:
def hi(name="yasoob"): верни "привет" + имя распечатать(привет()) # вывод: 'привет, ясуб' # Мы можем даже присвоить функцию переменной, например приветствовать = привет # Мы не используем здесь круглые скобки, потому что мы не вызываем функцию hi # вместо этого мы просто помещаем его в переменную приветствия. Попробуем запустить это распечатать (приветствовать ()) # вывод: 'привет, ясуб' # Посмотрим, что произойдет, если мы удалим старую функцию hi! дель привет распечатать(привет()) #выходы: ошибка имени распечатать (приветствовать ()) #outputs: 'привет, ясуб'
7.2. Определение функций внутри функций:
Таковы основы, когда речь заходит о функциях. давайте возьмем ваш знания на шаг впереди. В Python мы можем определять функции внутри другие функции:
def hi(name="yasoob"): print("теперь вы внутри функции hi()") приветствовать(): вернуть «теперь вы находитесь в функции приветствия ()» приветствие (): return "теперь вы находитесь в функции welcome()" распечатать (приветствовать ()) распечатать(добро пожаловать()) print("теперь вы снова в функции hi()") привет() #output:теперь вы внутри функции hi() # теперь вы находитесь в функции приветствия() # теперь вы находитесь в функции welcome() # теперь вы снова в функции hi() # Это показывает, что всякий раз, когда вы вызываете hi(), Greet() и Welcome() # также называются. Однако функции приветствия() и приветствия() # недоступны вне функции hi() например: приветствовать() #outputs: NameError: имя "greet" не определено
Итак, теперь мы знаем, что можем определять функции в других функциях. В Другими словами: мы можем создавать вложенные функции. Теперь вам нужно выучить один более того, функции тоже могут возвращать функции.
7.3. Возврат функций из функций:
Нет необходимости выполнять функцию внутри другой функции, мы также может вернуть его как вывод:
def hi(name="yasoob"): приветствовать(): вернуть «теперь вы находитесь в функции приветствия ()» приветствие (): return "теперь вы находитесь в функции welcome()" если имя == "ясуб": ответное приветствие еще: Добро пожаловать а = привет() печать (а) #outputs: <приветствие функции по адресу 0x7f2143c01500> #Это ясно показывает, что `a` теперь указывает на функцию приветствия() в hi() #А теперь попробуй распечатать (а()) #outputs: теперь вы находитесь в функции приветствия()
Просто взгляните на код еще раз. В предложении if/else
мы
возвращает приветствие
и приветствие
, а не приветствие()
и приветствие()
.
Почему это? Это потому, что, когда вы ставите пару скобок после него,
функция выполняется; тогда как если вы не поставите круглые скобки после него,
затем его можно передать и присвоить другим переменным
не выполняя его. Ты понял? Позвольте мне объяснить это в немного
Подробнее. Когда мы пишем a = hi()
, hi()
выполняется и
поскольку по умолчанию используется имя yasoob, возвращается функция приветствия
.
Если мы изменим выражение на a = hi(name = "ali")
, то приветствуем
функция будет возвращена. Мы также можем выполнить print hi()()
, который выводит теперь вы находитесь в функции приветствия() .
7.4. Передача функции в качестве аргумента другой функции:
def hi(): верни "привет ясуб!" def doSomethingBeforeHi(func): print("Я делаю кое-какую скучную работу перед выполнением hi()") печать (функция ()) сделать что-то до привета (привет) #outputs:Я делаю скучную работу перед выполнением hi() # привет ясуб!
Теперь у вас есть все необходимые знания, чтобы узнать, что такое декораторы на самом деле. являются. Декораторы позволяют выполнять код до и после функции.
7.5. Написание вашего первого декоратора:
В последнем примере мы действительно создали декоратор! Давайте изменим предыдущий декоратор и сделать программу более удобной:
def a_new_decorator(a_func): деф обернутьФункция(): print("Я делаю скучную работу перед выполнением a_func()") a_func() print("Я делаю скучную работу после выполнения a_func()") вернуть оберткуTheFunction определение a_function_requiring_decoration(): print("Я функция, которая нуждается в некоторой отделке, чтобы удалить мой неприятный запах") a_function_requiring_decoration() #outputs: "Я функция, которая нуждается в украшении, чтобы удалить мой неприятный запах" a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration) #теперь a_function_requiring_decoration обертывается функцией wrapTheFunction() a_function_requiring_decoration() #outputs:Я делаю скучную работу перед выполнением a_func() # Я функция, которая нуждается в некоторой отделке, чтобы удалить мой неприятный запах # Я делаю скучную работу после выполнения a_func()
Получил? Мы просто применили изученные ранее принципы. Этот это именно то, что делают декораторы в Python! Они обертывают функцию и тем или иным образом изменить свое поведение. Теперь вы можете быть интересно, почему мы нигде в нашем коде не использовали @? Это просто короткий способ оформления украшенной функции. Вот как мы могли бы запустите предыдущий пример кода, используя @.
@a_new_decorator определение a_function_requiring_decoration(): """Эй, ты! Укрась меня!""" print("Я функция, которая нуждается в украшении, чтобы " "удали мой неприятный запах") a_function_requiring_decoration() #outputs: я выполняю скучную работу перед выполнением a_func() # Я функция, которая нуждается в некоторой отделке, чтобы удалить мой неприятный запах # Я делаю скучную работу после выполнения a_func() #the @a_new_decorator — это всего лишь короткий способ сказать: a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
Надеюсь, теперь у вас есть общее представление о том, как работают декораторы в Питон. Теперь есть одна проблема с нашим кодом. Если мы запустим:
print(a_function_requiring_decoration.__name__) # Вывод: wrapTheFunction
Это не то, что мы ожидали! Его имя
«a_function_requiring_decoration». Итак, наша функция была заменена на
обернутьФункция. Он переопределял имя и строку документации нашей функции.
К счастью, Python предоставляет нам простую функцию для решения этой проблемы.
это functools.wraps
. Давайте изменим наш предыдущий пример, чтобы использовать functools.wraps
:
из пакетов импорта functools деф a_new_decorator(a_func): @обертки(a_func) деф обернутьФункция(): print("Я делаю скучную работу перед выполнением a_func()") a_func() print("Я делаю скучную работу после выполнения a_func()") вернуть оберткуTheFunction @a_new_decorator определение a_function_requiring_decoration(): """Эй, эй! Украсьте меня!""" print("Я функция, которая нуждается в украшении, чтобы " "удали мой неприятный запах") print(a_function_requiring_decoration. __name__) # Вывод: a_function_requiring_decoration
Теперь намного лучше. Давайте продолжим и изучим некоторые варианты использования декораторы.
Чертеж:
из пакетов импорта functools def декоратор_имя (f): @обертывания(ф) деф оформлен(*args, **kwargs): если не can_run: вернуть "Функция не будет работать" вернуть f(*args, **kwargs) вернуться украшенный @decorator_name Функция определения(): return("Функция запущена") can_run = Истина печать (функция ()) # Вывод: функция запущена can_run = Ложь печать (функция ()) # Вывод: функция не запустится
Примечание: @wraps
берет функцию для украшения и добавляет
возможность копирования имени функции, строки документации, аргументов
список и т. д. Это позволяет нам получить доступ к предварительно оформленным свойствам функции
в декораторе.
7.5.1. Варианты использования:
Теперь давайте посмотрим на области, в которых декораторы действительно проявляют себя и их использование делает что-то очень простым в управлении.
7.5.2. Авторизация
Декораторы могут помочь проверить, авторизован ли кто-либо для использования конечная точка в веб-приложении. Они широко используются в сети Flask. Фреймворк и Джанго. Вот пример использования декоратора на основе аутентификация:
Пример:
из пакетов импорта functools защита требует_auth (f): @обертывания(ф) деф оформлен(*args, **kwargs): авторизация = запрос.авторизация если нет авторизации или нет check_auth(auth.username, auth.password): аутентифицировать() вернуть f(*args, **kwargs) вернуться украшенный
7.6. Декораторы с аргументами
Если подумать, разве @wraps
тоже не является декоратором? Но это занимает
аргумент, как и любая нормальная функция. Итак, почему мы не можем сделать это тоже?
Это потому, что при использовании синтаксиса @my_decorator
вы
применение функции-оболочки с одной функцией в качестве параметра. Помните, что все в Python является объектом, включая
функции! Имея это в виду, мы можем написать функцию, которая возвращает
функция-обертка.
7.6.1. Вложение декоратора в функцию
Давайте вернемся к нашему примеру с журналированием и создадим оболочку, которая позволяет мы указываем файл журнала для вывода.
из пакетов импорта functools деф логит (файл журнала = 'out.log'): деф logging_decorator (функция): @обертывания (функция) def wrapper_function(*args, **kwargs): log_string = func.__name__ + "был вызван" печать (лог_строка) # Откройте лог-файл и добавьте с open(logfile, 'a') как open_file: # Теперь мы логируемся в указанный лог-файл open_file.write(log_string + '\n') функция возврата (*args, **kwargs) вернуть завернутую_функцию вернуть logging_decorator @логит() защита myfunc1(): проходить моя функция1() # Вывод: был вызван myfunc1 # Файл с именем out. log теперь существует с приведенной выше строкой @logit(logfile='func2.log') защита myfunc2(): проходить моя функция2() # Вывод: был вызван myfunc2 # Теперь существует файл func2.log с приведенной выше строкой
7.6.2. Классы декораторов
Теперь у нас есть наш логит-декоратор в производстве, но когда некоторые части нашего приложения считаются критическими, сбой может быть что-то, что требует более немедленного внимания. скажем иногда вы хотите просто войти в файл. В других случаях вы хотите отправить электронное письмо, так что проблема доведена до вашего сведения, и все равно ведите лог для собственных записей. Это случай использования наследования, но до сих пор мы видели только функции, используемые для создания декораторов.
К счастью, классы также можно использовать для создания декораторов. Итак, начнем перестройте логит как класс, а не как функцию.
логит класса (объект): _logfile = 'out.log' def __init__(я, функция): self.func = функция def __call__(я, *аргументы): log_string = self. func.__name__ + "был вызван" печать (лог_строка) # Откройте лог-файл и добавьте с open(self._logfile, 'a') как open_file: # Теперь мы логируемся в указанный лог-файл open_file.write(log_string + '\n') # Теперь отправьте уведомление самоуведомление() # возвращаем базовую функцию вернуть self.func(*args) деф уведомить (сам): # logit только логи, не более проходить
Эта реализация имеет дополнительное преимущество, заключающееся в том, что она намного чище, чем подход с вложенными функциями, и обертывание функции по-прежнему будет использовать тот же синтаксис, что и раньше:
logit._logfile = 'out2.log' # если изменить файл журнала @логит защита myfunc1(): проходить моя функция1() # Вывод: был вызван myfunc1
Теперь давайте создадим подкласс logit, чтобы добавить функциональность электронной почты (хотя эта тема здесь рассматриваться не будет).
класс email_logit(logit): ''' Реализация логита для отправки писем администраторам при вызове функции. ''' def __init__(self, email='[email protected]', *args, **kwargs): self.email = электронная почта super(email_logit, self).__init__(*args, **kwargs) деф уведомить (сам): # Отправить электронное письмо на адрес self.email # Здесь не будет реализовано проходить
Отсюда @email_logit
работает так же, как @logit
, но отправляет электронное письмо
администратору в дополнение к регистрации.
Декоратор
/ Шаблоны проектирования / Структурные модели
Также известен как: Обертка
IntentDecorator — это структурный шаблон проектирования, который позволяет добавлять к объектам новые варианты поведения, помещая эти объекты в специальные объекты-оболочки, содержащие варианты поведения.
ПроблемаПредставьте, что вы работаете над библиотекой уведомлений, которая позволяет другим программам уведомлять своих пользователей о важных событиях.
Первоначальная версия библиотеки была основана на классе Notifier
, который имел всего несколько полей, конструктор и единственный метод send
. Метод может принимать аргумент сообщения от клиента и отправлять сообщение в список электронных писем, которые были переданы уведомителю через его конструктор. Стороннее приложение, выступающее в роли клиента, должно было один раз создать и настроить объект-уведомитель, а затем использовать его каждый раз, когда происходит что-то важное.
Программа может использовать класс уведомления для отправки уведомлений о важных событиях на предопределенный набор электронных писем.
В какой-то момент вы понимаете, что пользователи библиотеки ожидают большего, чем просто уведомления по электронной почте. Многие из них хотели бы получать SMS о критических проблемах. Другие хотели бы получать уведомления на Facebook и, конечно же, корпоративные пользователи хотели бы получать уведомления Slack.
Каждый тип уведомления реализован как подкласс уведомителя.
Насколько это сложно? Вы расширили класс Notifier
и поместили дополнительные методы уведомления в новые подклассы. Теперь клиент должен был создать экземпляр нужного класса уведомлений и использовать его для всех дальнейших уведомлений.
Но тут кто-то резонно спросил вас: «Почему нельзя использовать сразу несколько типов уведомлений? Если ваш дом горит, вы, вероятно, захотите получать информацию по всем каналам».
Вы пытались решить эту проблему, создав специальные подклассы, которые объединяли несколько методов оповещения в одном классе. Однако быстро стало очевидно, что такой подход сильно раздует код, причем не только библиотечный, но и клиентский.
Комбинаторный взрыв подклассов.
Вам нужно найти другой способ структурировать классы уведомлений, чтобы их количество случайно не побило какой-нибудь рекорд Гиннеса.
РешениеРасширение класса — это первое, что приходит на ум, когда вам нужно изменить поведение объекта. Однако у наследования есть несколько серьезных предостережений, о которых вам необходимо знать.
- Наследование является статическим. Вы не можете изменить поведение существующего объекта во время выполнения. Вы можете заменить весь объект только другим, созданным из другого подкласса.
- Подклассы могут иметь только один родительский класс. В большинстве языков наследование не позволяет классу одновременно наследовать поведение нескольких классов.
Одним из способов обойти эти ограничения является использование Агрегация или Состав вместо Наследование . Обе альтернативы работают почти одинаково: один объект имеет ссылку на другой и делегирует ему некоторую работу, тогда как при наследовании сам объект — это , способный выполнять эту работу, наследуя поведение от своего суперкласса.
Благодаря этому новому подходу вы можете легко заменить связанный «вспомогательный» объект другим, изменив поведение контейнера во время выполнения. Объект может использовать поведение различных классов, имея ссылки на несколько объектов и делегируя им все виды работы. Агрегация/композиция — ключевой принцип многих шаблонов проектирования, включая Decorator. На этой ноте давайте вернемся к обсуждению шаблонов.
Наследование и объединение
«Обертка» — альтернативное название паттерна «Декоратор», ярко выражающее основную идею паттерна. Оболочка — это объект, который может быть связан с некоторым целевым объектом . Обертка содержит тот же набор методов, что и цель, и делегирует ей все запросы, которые она получает. Однако оболочка может изменить результат, выполнив какое-либо действие либо до, либо после передачи запроса цели.
Когда простая обертка становится настоящим декоратором? Как я уже упоминал, оболочка реализует тот же интерфейс, что и обернутый объект. Поэтому с точки зрения клиента эти объекты идентичны. Заставьте поле ссылки оболочки принимать любой объект, следующий за этим интерфейсом. Это позволит вам покрыть объект несколькими оболочками, добавляя к нему комбинированное поведение всех оболочек.
В нашем примере с уведомлениями давайте оставим простое поведение уведомления по электронной почте внутри базового класса Notifier
, но превратим все остальные методы уведомления в декораторы.
Различные методы уведомлений становятся декораторами.
Код клиента должен был бы обернуть базовый объект уведомления в набор декораторов, которые соответствуют предпочтениям клиента. Полученные объекты будут структурированы как стек.
Приложения могут настраивать сложные стеки декораторов уведомлений.
Последним декоратором в стеке будет объект, с которым фактически работает клиент. Поскольку все декораторы реализуют тот же интерфейс, что и базовый уведомитель, остальному клиентскому коду все равно, работает ли он с «чистым» объектом уведомителя или декорированным.
Мы могли бы применить тот же подход к другим действиям, таким как форматирование сообщений или составление списка получателей. Клиент может украсить объект любыми пользовательскими декораторами, если они следуют тому же интерфейсу, что и другие.
Аналогия из реального мираВы получаете комбинированный эффект от ношения нескольких предметов одежды.
Ношение одежды является примером использования декораторов. Когда тебе холодно, ты закутываешься в свитер. Если вам все же холодно со свитером, можно надеть сверху куртку. Если идет дождь, можно надеть плащ. Все эти предметы одежды «продлевают» ваше основное поведение, но не являются частью вас, и вы можете легко снять любой предмет одежды, когда он вам не нужен.
СтруктураКомпонент объявляет общий интерфейс как для оболочек, так и для обернутых объектов.
Бетонный компонент — это класс обертываемых объектов. Он определяет базовое поведение, которое может быть изменено декораторами.
Класс Base Decorator имеет поле для ссылки на обернутый объект. Тип поля должен быть объявлен как интерфейс компонента, чтобы оно могло содержать как конкретные компоненты, так и декораторы. Базовый декоратор делегирует все операции обернутому объекту.
Concrete Decorators определяют дополнительные параметры поведения, которые можно динамически добавлять к компонентам. Конкретные декораторы переопределяют методы базового декоратора и выполняют их поведение либо до, либо после вызова родительского метода.
Клиент может заключать компоненты в несколько слоев декораторов, если он работает со всеми объектами через интерфейс компонента.
В этом примере 9Шаблон 0085 Decorator позволяет сжимать и шифровать конфиденциальные данные независимо от кода, который фактически использует эти данные.
Пример декораторов шифрования и сжатия.
Приложение оборачивает объект источника данных парой декораторов. Обе оболочки изменяют способ записи и чтения данных с диска:
Непосредственно перед записью данных на диск декораторы шифруют и сжимают их. Исходный класс записывает зашифрованные и защищенные данные в файл, не зная об изменении.
Сразу после того, как данные считаны с диска , они проходят через те же декораторы, которые распаковывают и декодируют их.
Декораторы и класс источника данных реализуют один и тот же интерфейс, что делает их взаимозаменяемыми в клиентском коде.
// Интерфейс компонента определяет операции, которые могут быть
// изменено декораторами.
Интерфейс DataSource
метод writeData(данные)
метод readData():данные
// Конкретные компоненты обеспечивают реализацию по умолчанию для
// операции. Может быть несколько вариантов этих
// классы в программе.
класс FileDataSource реализует DataSource
конструктор FileDataSource(имя файла) { ... }
метод writeData(данные)
// Записать данные в файл.
метод readData():данные
// Чтение данных из файла.
// Базовый класс декоратора следует тому же интерфейсу, что и класс
// другие компоненты. Основная цель этого класса состоит в том, чтобы
// определяем интерфейс обертывания для всех конкретных декораторов.
// Реализация кода упаковки по умолчанию может включать
// поле для хранения обернутого компонента и средства для
// инициализируем его.
класс DataSourceDecorator реализует DataSource
оболочка защищенного поля: DataSource
конструктор DataSourceDecorator (источник: DataSource)
обертка = источник
// Базовый декоратор просто делегирует всю работу
// упакованный компонент. Дополнительные варианты поведения могут быть добавлены в
// декораторы бетона.
метод writeData(данные)
wrappee.writeData(данные)
// Конкретные декораторы могут вызывать родительскую реализацию
// операция вместо вызова обернутого объекта
// напрямую. Этот подход упрощает расширение декоратора
// классы.
метод readData():данные
вернуть обертку.readData()
// Конкретные декораторы должны вызывать методы обернутого объекта,
// но могут добавить к результату что-то свое. Декораторы
// может выполнять добавленное поведение либо до, либо после
// обращение к обернутому объекту.
класс EncryptionDecorator расширяет DataSourceDecorator.
метод writeData(данные)
// 1. Зашифровать передаваемые данные.
// 2. Передаем зашифрованные данные в writeData обертки
// метод.
метод readData():данные
// 1. Получить данные из метода readData оболочки.
// 2. Попробуйте расшифровать его, если он зашифрован.
// 3. Вернуть результат.
// Вы можете обернуть объекты несколькими слоями декораторов.
класс CompressionDecorator расширяет DataSourceDecorator
метод writeData(данные)
// 1. Сжать переданные данные.
// 2. Передаем сжатые данные в writeData обертки
// метод.
метод readData():данные
// 1. Получить данные из метода readData оболочки.
// 2. Попробуйте распаковать его, если он сжат.
// 3. Вернуть результат.
// Вариант 1. Простой пример сборки декоратора.
приложение класса
метод тупойUsageExample() является
источник = новый FileDataSource("somefile.dat")
source.writeData(salaryRecords)
// Целевой файл был записан с обычными данными.
источник = новый CompressionDecorator(источник)
source.writeData(salaryRecords)
// Целевой файл был записан со сжатым
// данные.
источник = новый EncryptionDecorator (источник)
// Исходная переменная теперь содержит это:
// Шифрование > Сжатие > FileDataSource
source.writeData(salaryRecords)
// Файл был записан со сжатием и
// зашифрованные данные.
// Вариант 2. Клиентский код, использующий внешний источник данных.
// Объекты SalaryManager не знают и не заботятся о данных
// специфика хранения. Они работают с предварительно настроенными данными
// источник, полученный из конфигуратора приложения.
класс SalaryManager
источник поля: DataSource
конструктор SalaryManager(источник: DataSource) { ... }
метод загрузки ()
вернуть источник.readData()
метод save() есть
source.writeData(salaryRecords)
// ...Другие полезные методы...
// Приложение может собирать разные стеки декораторов на
// время выполнения, в зависимости от конфигурации или среды.
класс ApplicationConfigurator это
пример конфигурации метода ()
источник = новый FileDataSource ("salary.dat")
если (enabledEncryption)
источник = новый EncryptionDecorator (источник)
если (включено сжатие)
источник = новый CompressionDecorator(источник)
регистратор = новый SalaryManager (источник)
зарплата = logger.load()
// ...
ПрименимостьИспользуйте шаблон Decorator, когда вам нужно иметь возможность назначать дополнительное поведение объектам во время выполнения, не нарушая код, использующий эти объекты.
Декоратор позволяет структурировать вашу бизнес-логику по слоям, создавать декоратор для каждого слоя и создавать объекты с различными комбинациями этой логики во время выполнения. Клиентский код может обрабатывать все эти объекты одинаково, поскольку все они имеют общий интерфейс.
Используйте шаблон, когда неудобно или невозможно расширить поведение объекта с помощью наследования.
Многие языки программирования имеют ключевое слово final
, которое можно использовать для предотвращения дальнейшего расширения класса. Для окончательного класса единственным способом повторного использования существующего поведения будет обернуть класс вашей собственной оболочкой, используя шаблон Decorator.
Убедитесь, что ваш бизнес-домен может быть представлен как основной компонент с несколькими дополнительными слоями над ним.
Выясните, какие методы являются общими как для основного компонента, так и для дополнительных слоев. Создайте интерфейс компонента и объявите там эти методы.
Создайте конкретный класс компонентов и определите в нем базовое поведение.
Создайте базовый класс декоратора. В нем должно быть поле для хранения ссылки на обернутый объект. Поле должно быть объявлено с типом интерфейса компонента, чтобы разрешить связь с конкретными компонентами, а также с декораторами. Базовый декоратор должен делегировать всю работу обернутому объекту.
Убедитесь, что все классы реализуют интерфейс компонента.
Создавайте конкретные декораторы, расширяя их от базового декоратора. Конкретный декоратор должен выполнять свое поведение до или после вызова родительского метода (который всегда делегирует обернутому объекту).
Код клиента должен отвечать за создание декораторов и компоновку их так, как нужно клиенту.
- Вы можете расширить поведение объекта без создания нового подкласса.
- Вы можете добавлять или удалять обязанности объекта во время выполнения.
- Вы можете комбинировать несколько вариантов поведения, заключив объект в несколько декораторов.
- Принцип единой ответственности . Вы можете разделить монолитный класс, реализующий множество возможных вариантов поведения, на несколько более мелких классов.
- Трудно удалить конкретную оболочку из стека оболочек.
- Трудно реализовать декоратор таким образом, чтобы его поведение не зависело от порядка в стеке декораторов.
- Исходный код конфигурации слоев может выглядеть довольно уродливо.
Адаптер изменяет интерфейс существующего объекта, а Декоратор расширяет объект без изменения его интерфейса. Кроме того, Decorator поддерживает рекурсивную композицию, что невозможно при использовании адаптера .
- Адаптер
предоставляет другой интерфейс для обернутого объекта, Proxy предоставляет ему тот же интерфейс, а Decorator предоставляет расширенный интерфейс.
Chain of Responsibility и Decorator имеют очень похожую структуру классов. Оба шаблона полагаются на рекурсивную композицию, чтобы передать выполнение через ряд объектов. Однако есть несколько принципиальных отличий.
Обработчики CoR могут выполнять произвольные операции независимо друг от друга. Они также могут прекратить передачу запроса дальше в любой момент. С другой стороны, различные декораторы могут расширять поведение объекта, сохраняя при этом его согласованность с базовым интерфейсом. Кроме того, декораторы не могут нарушать поток запроса.
Composite и Decorator имеют схожие структурные диаграммы, поскольку оба полагаются на рекурсивную композицию для организации неограниченного количества объектов.
Декоратор похож на Composite , но имеет только один дочерний компонент. Есть еще одно существенное отличие: Decorator добавляет дополнительные обязанности обернутому объекту, а Composite просто «суммирует» результаты своих дочерних элементов.
Однако шаблоны также могут взаимодействовать: вы можете использовать Decorator для расширения поведения определенного объекта в Composite дереве.
Проекты, в которых интенсивно используются Composite и Decorator, часто могут выиграть от использования Prototype. Применение шаблона позволяет клонировать сложные структуры вместо того, чтобы воссоздавать их с нуля.
Decorator позволяет изменить внешний вид объекта, а Strategy позволяет изменить его внутреннюю часть.
Decorator и Proxy имеют схожие структуры, но совершенно разные цели. Оба паттерна построены по принципу композиции, где предполагается, что один объект делегирует часть работы другому.