Разворачивание декораторов. Часть 2
Nuances of programmingПеревод статьи Ryan Palo: Unwrapping Decorators, Part 2
Вспомним о чем шла речь
Предыдущий пост я писал про основы декораторов в 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, если вы делаете свою библиотеку.
В любом случае, если у вас остались какие-то вопросы по декораторам (или чему-либо еще), не стесняйтесь, напишите мне. Я всегда рад помочь (даже если я сразу не знаю ответ на вопрос, и мне нужно будет гуглить все это для того, чтобы ответить максимально подробно). Также пишите, если вам есть, что добавить по теме данной статьи.