Всем привет! В этой статье я расскажу об инструменте, разработанном мной, который изменяет работу декораторов в Python и делает их более Питоничными.
Я не буду рассказывать про области применения декораторов. Есть множество статей на эту тему.
Для начала, давайте вспомним: что же такое декораторы в Пайтон.
Если совсем просто, то это удобный способ передать одну функцию в другую и получить третью. В этом определении нет ни одного слова правды, но мы вернёмся к этому позже.
Давайте разбираться!
Как работают декораторы
def decorator_function(wrapped_func): def wrapper(): print('Входим в функцию-обёртку') print('Оборачиваемая функция: ', wrapped_func) print('Выполняем обёрнутую функцию...') wrapped_func() print('Выходим из обёртки') return wrapper
Так выглядит функция-декоратор. Как вы можете увидеть, она принимает в качестве аргумента другую функцию. Затем с этой функцией что-то делают внутри вложенной функции-обёртки и возвращают из декоратора уже обёртку вместо исходной функции.
Теперь можно декорировать:
@decorator_functiondef hello_world(): print('Hello world!')hello_world()
Здесь декоратор получает функцию hello_world
, и
подменяет её своей вложенной функцией wrapper
.
Вывод:
Входим в функцию-обёрткуОборачиваемая функция: <function hello_world at 0x0201B2F8>Выполняем обёрнутую функцию...Hello world!Выходим из обёртки
Важно помнить!
Декоратор исполняется только один раз: при объявлении
оборачиваемой функции. При дальнейшем вызове функции исполняется
только вложенная функция wrapper
.
Мы это увидим, если добавим две строчки в наш декоратор:
def decorator_function(wrapped_func): print('Входим в декоратор') def wrapper(): ... print('Выходим из декоратора') return wrapper
@decorator_functiondef hello_world(): print('Hello world!')
Входим в декораторВыходим из декоратора
hello_world()
Входим в функцию-обёрткуОборачиваемая функция: <function hello_world at 0x0201B2F8>Выполняем обёрнутую функцию...Hello world!Выходим из обёртки
А вот и страдания: аргументы функции и аргументы декоратора
У функции, которую мы декорируем, могут быть аргументы.
Принимает их вложенная функция wrapper
:
def decorator_function(wrapped_func): def wrapper(*args): ... wrapped_func(args) ... return wrapper @decorator_functiondef hello_world(text): print(text) hello_world('Hello world!')
А ещё, аргументы могут быть переданы непосредственно в декоратор:
def fictive(decorator_text): def decorator_function(wrapped_func): def wrapper(*args): print(decorator_text, end='') wrapped_func(*args) return wrapper return decorator_function@fictive(decorator_text='Hello, ')def hello_world(text): print(text)hello_world('world!')
Здесь аргумент decorator_text
передаётся при
декорировании в строке 11 и попадает в функцию
fictive
, строка 1. Таким образом, появился ещё один
уровень вложенности только для того, чтобы принять аргументы
декоратора.
Вывод:
Hello, world!
Пойдём дальше. А что, если декоратор может быть, в одних случаях с аргументами, в других - без аргументов? Поехали!
def fictive(_func=None, *, decorator_text=''): def decorator_function(wrapped_func): def wrapper(*args): print(decorator_text, end='') wrapped_func(*args) return wrapper if _func is None: return decorator_function else: return decorator_function(_func) @fictivedef hello_world(text): print(text)hello_world('Hello, world!')@fictive(decorator_text='Hi, ')def say(text): print(text)say('world!')
Вывод:
Hello, world!Hi, world!
Как Вам код? Вспомним, мантру Питонистов из начала статьи:
Декораторы - это удобный способ передать...
Ничего, на помощь придёт DecoratorHelper! Но, перед этим, ещё пара слов о декораторах.
Мифы декораторов
-
Декораторы удобны. Думаю, с этим мы уже разобрались.
-
В декораторы нужно передавать функции. Передавать можно не только функции, но и любые callable объекты. Это такие объекты, у которых определён дандер метод (магический метод)
__call__
. Этот метод отвечает за операции, которые будут произведены при вызове объекта (когда вы ставите скобочки после имени объекта:object()
). Вместо функции может быть метод или класс. -
Декораторы - это функции. И опять: это может быть любой callable объект.
-
Декоратор возвращает функцию. Декоратор может возвращать что угодно. Стоит лишь помнить, что если декоратор возвращает не callable объект, то вызывать его не получится.
-
Передавать можно не только функции, но и аргументы. Вместо функции может быть метод или некоторые классы.
DecoratorHelper: решение проблем
Устанавливаем модуль:
pip install DecoratorHelper
Импортируем и используем как декоратор:
from DecoratorHelper import DecoratorHelper@DecoratorHelperdef hello_world(text): print(text)hello_world('Hello, world!')
Что это даёт?
-
Вы больше не думаете над тем, будут ли аргументы у декоратора. DecoratorHelper думает об этом вместо Вас.
-
Вы получаете удобный, Питоничный доступ ко всем аргументам, самой функции, к тому, что будет происходить до и после выполнения функции.
В итоге Вы получаете вместо функции объект, который имеет следующие атрибуты:
-
self.function - оборачиваемая функция
-
self.decorator_args- аргументы декоратора. Кортеж позиционных аргументов, последний элемент которого - словарь с именованными аргументами.
-
self.function_args- аргументы функции. Кортеж позиционных аргументов, последний элемент которого - словарь с именованными аргументами.
-
self.pre_function - то, что будет происходить перед выполнением функции (так можно превратить функцию в коллбэк).
-
self.post_function - то, что будет происходить после выполнения функции (так можно добавить функции в коллбэк).
Как использовать?
Перепишем приведённый ранее код декоратора, который может принимать/не принимать аргументы:
from DecoratorHelper import DecoratorHelperdef fictive(object): object.pre_function = lambda : print(*object.decorator_args[:-1], end='') return object @fictive@DecoratorHelper('Hello, ')def hello_world(text): print(text)hello_world('world!')
Как мы можем видеть, тело декоратора сократилось в 8 раз. Profit!
Ограничение! Первым аргументом нельзя передавать callable объекты, иначе всё сломается :) Думаю, для большинства задач, это не смертельно...
Что дальше?
В следующих версиях планируется:
-
Улучшенная обработка аргументов.
-
Встроенный счётчик вызовов.
-
Возможность превратить объект в синглтон.
-
Возможность превратить объект в буилдер.
-
Может быть, возможность подключить асинхронность.
И всё это в максимально удобном формате:
singleton = True.
P. S. Если в комментариях будет интерес к теме, напишу вторую статью о том, как DecoratorHelper устроен. Но сразу скажу, что это уровень Junior+.