Разворачивание декораторов. Часть 2

06.04.2018

Перевод статьи 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, если вы делаете свою библиотеку.

В любом случае, если у вас остались какие-то вопросы по декораторам (или чему-либо еще), не стесняйтесь, напишите мне. Я всегда рад помочь (даже если я сразу не знаю ответ на вопрос, и мне нужно будет гуглить все это для того, чтобы ответить максимально подробно). Также пишите, если вам есть, что добавить по теме данной статьи.