Декораторы

Декораторы

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

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

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

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

def my_shiny_new_decorator(function_to_decorate):
    # Внутри себя декоратор определяет функцию-"обёртку". Она будет обёрнута вокруг декорируемой,
    # получая возможность исполнять произвольный код до и после неё.
    def the_wrapper_around_the_original_function():
        print("Я - код, который отработает до вызова функции")
        function_to_decorate() # Сама функция
        print("А я - код, срабатывающий после")
    # Вернём эту функцию
    return the_wrapper_around_the_original_function

# Представим теперь, что у нас есть функция, которую мы не планируем больше трогать.
def stand_alone_function():
    print("Я простая одинокая функция, ты ведь не посмеешь меня изменять?")

stand_alone_function()
# Однако, чтобы изменить её поведение, мы можем декорировать её, то есть просто передать декоратору,
# который обернет исходную функцию в любой код, который нам потребуется, и вернёт новую,
# готовую к использованию функцию:
stand_alone_function_decorated = my_shiny_new_decorator(stand_alone_function)
stand_alone_function_decorated()

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

stand_alone_function = my_shiny_new_decorator(stand_alone_function)
stand_alone_function()

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

@my_shiny_new_decorator
def another_stand_alone_function():
    print("Оставь меня в покое")

another_stand_alone_function()

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

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

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

def bread(func):
    def wrapper():
        print()
        func()
        print("<\______/>")
    return wrapper

def ingredients(func):
    def wrapper():
        print("#помидоры#")
        func()
        print("~салат~")
    return wrapper

def sandwich(food="--ветчина--"):
    print(food)

sandwich()
sandwich = bread(ingredients(sandwich))
sandwich()

И аналогично через декораторы:

@bread
@ingredients
def sandwich(food="--ветчина--"):
    print(food)

sandwich()

Не забываем, что так как порядок вызова функций имеет значение, то и порядок проставление декораторов так же имеет значение.

Упражнение 2

Напишите функцию, которая получает на вход список чисел и выдает ответ сколько в данном списке четных чисел. Напишите декоратор, который меняет поведение функции следующим образом: если четных чисел нет, то пишет "Нет(" а если их больше 10, то пишет "Очень много"

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

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

Текстовый данные в языке пайтон описываются классом str:

def a_decorator_passing_arguments(function_to_decorate):
    def a_wrapper_accepting_arguments(arg1, arg2):
        print("Смотри, что я получил:", arg1, arg2)
        function_to_decorate(arg1, arg2)
    return a_wrapper_accepting_arguments

# Теперь, когда мы вызываем функцию, которую возвращает декоратор, мы вызываем её "обёртку",
# передаём ей аргументы и уже в свою очередь она передаёт их декорируемой функции
@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
    print("Меня зовут", first_name, last_name)

print_full_name("Vasya", "Pupkin")

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

def decorator_maker():
    print("Я создаю декораторы! Я буду вызван только раз: когда ты попросишь меня создать декоратор.")
    def my_decorator(func):
        print("Я - декоратор! Я буду вызван только раз: в момент декорирования функции.")
        def wrapped():
            print ("Я - обёртка вокруг декорируемой функции.\n"
                   "Я буду вызвана каждый раз, когда ты вызываешь декорируемую функцию.\n"
                   "Я возвращаю результат работы декорируемой функции.")
            return func()
        print("Я возвращаю обёрнутую функцию.")
        return wrapped
    print("Я возвращаю декоратор.")
    return my_decorator

# Давайте теперь создадим декоратор. Это всего лишь ещё один вызов функции
new_decorator = decorator_maker()
# Теперь декорируем функцию
def decorated_function():
    print("Я - декорируемая функция.")

decorated_function = new_decorator(decorated_function)
# Теперь наконец вызовем функцию:
decorated_function()

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

@decorator_maker()
def decorated_function():
    print("Я - декорируемая функция.")

decorated_function()

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

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):
    print("Я создаю декораторы! И я получил следующие аргументы:",
           decorator_arg1, decorator_arg2)
    def my_decorator(func):
        print("Я - декоратор. И ты всё же смог передать мне эти аргументы:",
               decorator_arg1, decorator_arg2)
        # Не перепутайте аргументы декораторов с аргументами функций!
        def wrapped(function_arg1, function_arg2):
            print ("Я - обёртка вокруг декорируемой функции.\n"
                   "И я имею доступ ко всем аргументам\n"
                   "\t- и декоратора: {0} {1}\n"
                   "\t- и функции: {2} {3}\n"
                   "Теперь я могу передать нужные аргументы дальше"
                   .format(decorator_arg1, decorator_arg2,
                           function_arg1, function_arg2))
            return func(function_arg1, function_arg2)
        return wrapped
    return my_decorator

@decorator_maker_with_arguments("Леонард", "Шелдон")
def decorated_function_with_arguments(function_arg1, function_arg2):
    print ("Я - декорируемая функция и я знаю только о своих аргументах: {0}"
           " {1}".format(function_arg1, function_arg2))

decorated_function_with_arguments("Раджеш", "Говард")

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

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

Упражнение 3

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

Пример ожидаемого поведения:

@swap
def div(x, y, show=False):
    res = x / y
    if show:
        print(res)
    return res

div(2, 4, show=True)
>>> 2.0

Упражнение 4

Напишите декоратор, который принимает в качестве аргумента путь к файлу. Если данный декоратор добавить к функции, то в указанный файл будет логироваться информация вида:
  1. Время вызова функции
  2. Входящие аргументы
  3. Ответ return (если есть, если нет то логгировать '-')
  4. Время завершения работы функции
  5. Время работы функции