Предыдущие части: Часть 1

Вспомним о чем шла речь

Предыдущий пост я писал про основы декораторов в Python. Для тех, кто не читал её, в двух словах расскажу что там было.

1. Декораторы расположены перед объявлением функций и служат для того, чтобы добавить какой-то функционал не скрывая цель исходной функции.

2. Выглядит это примерно вот так:

@custom_decorator
def generic_example_function():
    # ...
    pass

3. При определении функции декоратора, она должна использоваться как ввод и выводить новую, измененную функцию.

def custom_decorator(func):
    # *args, **kwargs allow your decorated function to handle
    # the inputs it is supposed to without problems

    def modified_function(*args, **kwargs):
        # Do some extra stuff
        # ...
        return func(*args, **kwargs) 
# Call the input function as it
# was originally called and return that

    return modified_function

Ок, теперь перейдем к более серьезным темам. Сегодня поговорим о передаче значений переменных декоратору, стеке декораторов и декораторе на основе классов.

Аргументы декоратора

Вы можете передавать значения переменных декоратору! Становится немного сложнее, но ничего страшного. Помните, как основная функция декоратора принимает функцию, определяет новую и возвращает ее? Если у вас есть переменные, вам фактически нужно создать декоратор прямо на ходу, для этого вам нужно определить функцию, которая возвращает функцию декоратора, которая, в свою очередь, возвращает фактическую функцию, которая вам и нужна.

from time import sleep

def delay(seconds): 
# The outermost function handles the decorator's arguments

    def delay_decorator(func): 
# It defines a decorator function, like we are used to

        def inner(*args, **kwargs): 
# The decorator function defines the modified function
# Because we do things this way, the inner function
# gets access to the arguments supplied to the decorator initially
            sleep(seconds)
            return func(*args, **kwargs)

        return inner  
# Decorator function returns the modified function

    return delay_decorator 
# Finally, the outer function returns the custom decorator

@delay(5)
def sneeze(times):
    return "Achoo! " * times

>>> sneeze(3)
(wait 5 seconds)
"Achoo! Achoo! Achoo!"

Опять же, на первый взгляд это может показать очень сложным и запутанным, но постарайтесь в этом разобраться. Скорее всего вы рассуждаете так: внешняя функция delay в этом случае ведет себя так, как будто она вызывается прямо при объявлении декоратора. Как только программа считывает @delay(5), он выполняет функцию задержки и заменяет @delay на измененное, возвращенное значение декоратора. В ходе работы программы, когда мы вызываем sneeze, это выглядит, как будто sneezе спрятано в delay_decorator с секундами = 5. Таким образом, фактическая, вызываемая функция — это inner, которую мы понимаем как sneeze, спрятанную в 5-секундную функцию «сна». Все еще ничего не понимаете? Я тоже. Может просто сходить поспать, а после вернуться и прочитать это еще раз.

Стек декораторов

Думаю, нам стоит перейти к чему-то более простому. Надеюсь, вы будете в фоновом режиме обдумывать предыдущий параграф, и под конец всей статьи вы магическим образом поймете, что там написано. Посмотрим, что из этого выйдет. Давайте все-таки поговорим про стек. Я, наверное, просто покажу вам часть кода и вы поймете суть.

def pop(func):

    def inner(*args, **kwargs):
        print("Pop!")
        return func(*args, **kwargs)

    return inner

def lock(func):

    def inner(*args, **kwargs):
        print("Lock!")
        return func(*args, **kwargs)

    return inner

@pop
@lock
def drop(it):
    print("Drop it!")
    return it[:-2]

>>> drop("This example is obnoxious, isn't it")
Pop!
Lock!
Drop it
"This example is obnoxious, isn't "

Как вы можете заметить, мы можем спрятать функцию, которая уже была спрятана. В математике (и, собственно, в программировании) это называют композицией функций. Так же как f o g(x) == f(g(x)), если положить в стек @pop на @lock на drop, то получится pop(lock(drop(it))).

Декоратор на основе классов

…без переменных

На самом деле декоратор можно создать без всяких переменных. Обычно я стараюсь приводить свои собственные примеры, но тот, что я нашел, был чертовски хорош, поэтому я буду использовать его.

class MySuperCoolDecorator:
    def __init__(self, func):
        print("Initializing decorator class")
        self.func = func
        func()

    def __call__(self):
        print("Calling decorator call method")
        self.func()

@MySuperCoolDecorator
def simple_function():
    print("Inside the simple function")

print("Decoration complete!")

simple_function()

Вывод:

Initializing decorator class
Inside the simple function
Decoration complete!
Calling decorator call method
Inside the simple function

…с переменными

В декораторе на основе классов делать переменную-декоратор гораздо проще, но ведут они себя совершенно по-другому.

ПРЕДУПРЕЖДЕНИЕ: Декораторы на основе классов ведут себя по-разному в зависимости от того, есть у них переменные или нет.

Я точно не знаю, почему так происходит. Кто-нибудь поумнее, наверное, сможет это объяснить. В любом случае, когда в декораторе есть переменные, происходит три вещи.

1. Переменные декораторов передаются в функцию __init__.

2. Функция сама по себе является функцией вызова.

3. Функция __сall__ вызывается немедленно и только один раз, примерно так же работает декоратор в функциях.

В данном примере показано, что он создает простой кэширующий декоратор, похожий на встроенный @lru_cache, за исключением того, что вы можете предварительно загрузить его парами ввода / вывода.

class PreloadedCache:
    # This method is called as soon as the decorator is attached to a function.
    def __init__(self, preloads={}):
        """Expects a dictionary of preloaded {input: output} pairs.
        I know it only works for one input, but I'm keeping it simple."""
        if preloads is None:
            self.cache = {}
        else:
            self.cache = preloads

    def __call__(self, func):
        # This method is called when a function is passed to the decorator
        def inner(n):
            if n in self.cache:
                return self.cache[n]
            else:
                result = func(n)
                self.cache[n] = result
                return result
        return inner

@PreloadedCache({1: 1, 2: 1, 4: 3, 8: 21}) # First __init__, then __call__
def fibonacci(n):
    """Returns the nth fibonacci number"""
    if n in (1, 2):
        return 1
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)

# At runtime, the 'inner' function above will actually be called!
# fibonacci(8) never actually gets called, because it's already in the cache!

Довольно круто, правда? Я считаю, что такой вариант создания декоратора, по крайне мере для меня, является интуитивно понятным.

Подведем итоги

Да, я понимаю, что это много. Для меня эта тема является одной из самых запутанных в Python, но это вам очень сильно поможет при создании API, если вы делаете свою библиотеку.

Перевод статьи Ryan Palo: Unwrapping Decorators, Part 1