Как стать автором
Обновить

Декораторы Python: хватит это терпеть

Время на прочтение4 мин
Количество просмотров21K
Конец страданиям.
Конец страданиям.

Всем привет! В этой статье я расскажу об инструменте, разработанном мной, который изменяет работу декораторов в Python и делает их более «Питоничными».

Я не буду рассказывать про области применения декораторов. Есть множество статей на эту тему.

Для начала, давайте вспомним: что же такое декораторы в Пайтон.

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

Давайте разбираться!

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

def decorator_function(wrapped_func):
    def wrapper():
        print('Входим в функцию-обёртку')
        print('Оборачиваемая функция: ', wrapped_func)
        print('Выполняем обёрнутую функцию...')
        wrapped_func()
        print('Выходим из обёртки')
    return wrapper

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

Теперь можно декорировать:

@decorator_function
def 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_function
def 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_function
def 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)
      
@fictive
def 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! Но, перед этим, ещё пара слов о декораторах.

Мифы декораторов

  1. Декораторы удобны. Думаю, с этим мы уже разобрались.

  2. В декораторы нужно передавать функции. Передавать можно не только функции, но и любые callable объекты. Это такие объекты, у которых определён дандер метод (магический метод) __call__. Этот метод отвечает за операции, которые будут произведены при вызове объекта (когда вы ставите скобочки после имени объекта: object()). Вместо функции может быть метод или класс.

  3. Декораторы - это функции. И опять: это может быть любой callable объект.

  4. Декоратор возвращает функцию. Декоратор может возвращать что угодно. Стоит лишь помнить, что если декоратор возвращает не callable объект, то вызывать его не получится.


DecoratorHelper: решение проблем

Ссылка на GitHub: https://github.com/IvanDushenko/MyModules/blob/master/DecoratorHelper/__init__.py

Устанавливаем модуль:

pip install DecoratorHelper

Импортируем и используем как декоратор:

from DecoratorHelper import DecoratorHelper

@DecoratorHelper
def hello_world(text):
        print(text)
hello_world('Hello, world!')

Что это даёт?

  1. Вы больше не думаете над тем, будут ли аргументы у декоратора. DecoratorHelper думает об этом вместо Вас.

  2. Вы получаете удобный, Питоничный доступ ко всем аргументам, самой функции, к тому, что будет происходить до и после выполнения функции.

В итоге Вы получаете вместо функции объект, который имеет следующие атрибуты:

  • self.function - оборачиваемая функция

  • self.decorator_args - аргументы декоратора. Кортеж позиционных аргументов, последний элемент которого - словарь с именованными аргументами.

  • self.function_args - аргументы функции. Кортеж позиционных аргументов, последний элемент которого - словарь с именованными аргументами.

  • self.pre_function - то, что будет происходить перед выполнением функции (так можно превратить функцию в коллбэк).

  • self.post_function - то, что будет происходить после выполнения функции (так можно добавить функции в коллбэк).

Как использовать?

Перепишем приведённый ранее код декоратора, который может принимать/не принимать аргументы:

from DecoratorHelper import DecoratorHelper

def 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+.

Теги:
Хабы:
-2
Комментарии15

Публикации

Истории

Работа

Python разработчик
142 вакансии
Data Scientist
63 вакансии

Ближайшие события