Зачем нужны декораторы?
Кто читал классическое объяснение про декораторы:
# Декоратор - это функция, ожидающая ДРУГУЮ функцию в качестве параметра
def my_shiny_new_decorator(a_function_to_decorate):
# Внутри себя декоратор определяет функцию-"обёртку".
# Она будет (что бы вы думали?..) обёрнута вокруг декорируемой,
# получая возможность исполнять произвольный код до и после неё.
def the_wrapper_around_the_original_function():
# Поместим здесь код, который мы хотим запускать ДО вызова
# оригинальной функции
print "Я - код, который отработает до вызова функции"
# ВЫЗОВЕМ саму декорируемую функцию
a_function_to_decorate()
# А здесь поместим код, который мы хотим запускать ПОСЛЕ вызова
# оригинальной функции
print "А я - код, срабатывающий после"
# На данный момент функция "a_function_to_decorate" НЕ ВЫЗЫВАЛАСЬ НИ РАЗУ
# Теперь, вернём функцию-обёртку, которая содержит в себе
# декорируемую функцию, и код, который необходимо выполнить до и после.
# Всё просто!
return the_wrapper_around_the_original_function
# Представим теперь, что у нас есть функция, которую мы не планируем больше трогать.
def a_stand_alone_function():
print "Я простая одинокая функция, ты ведь не посмеешь меня изменять?.."
a_stand_alone_function()
# выведет: Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
# Однако, чтобы изменить её поведение, мы можем декорировать её, то есть
# Просто передать декоратору, который обернет исходную функцию в любой код,
# который нам потребуется, и вернёт новую, готовую к использованию функцию:
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#выведет:
# Я - код, который отработает до вызова функции
# Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
# А я - код, срабатывающий после
Далее идет пассаж:
Наверное, теперь мы бы хотели, чтобы каждый раз, во время вызова
a_stand_alone_function, вместо неё вызывалась
a_stand_alone_function_decorated. Нет ничего проще, просто перезапишем
a_stand_alone_function функцией, которую нам вернул
my_shiny_new_decorator:
a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
#выведет:
# Я - код, который отработает до вызова функции
# Я простая одинокая функция, ты ведь не посмеешь меня изменять?..
# А я - код, срабатывающий после
Т.е. после этого пассажа - мы теряем возможность вызвать функцию в первоначальном
виде. Теперь она ВСЕГДА декорирована.
И вопрос, зачем тогда декоратор был нужен?
1) Почему бы (если мы все равно теряем первоначальную функцию) просто не переписать
изначальную функцию? (просто дописав в начале и в конце функции необходимые нам куски
кода). Вот так:
def a_stand_alone_function():
print "Я - код, который отработает до вызова функции"
print "Я простая одинокая функция, ты ведь не посмеешь меня изменять?.."
print "А я - код, срабатывающий после"
или, если дополнительный код большой и должен лежать отдельно, то:
def a_stand_alone_function():
pre_function()
print "Я простая одинокая функция, ты ведь не посмеешь меня изменять?.."
post_function()
2) Зачем вся эта свистопляска, вместо того, чтобы сделать простое решение, как в
1 вопросе?
3) Я бы еще понял декораторы, если бы была возможность вызывать как декорируемую,
так и изначальную функцию. Но декораторы, написанные с @ этого не позволяют. Зачем
декоратор затирает оригинальную функцию?
Буду очень благодарен за ответ с объяснениями и, может быть, ПОНЯТНЫМИ примерами
из реальной практики.
Ответы
Ответ 1
Например, в Django декораторы широко используются. Вместо того, чтобы изобретать
велосипед и в каждой функции-контроллере, которая должна быть доступна только авторизованным
пользователям, писать свой код проверки авторизации, просто декорируем её, соответствующим
декоратором. А если нужно ещё гарантировать, что функция будет применяться только для
запросов определённого типа? И что вся работа с БД в функции будет выполняться в транзакции?
Если весь код тянуть в функцию, то вскоре её изначальный смысл просто потеряется в
куче шаблонного кода. А она ведь ещё и не одна в модуле, и для каждой придётся написать
этот шаблонный код, повторив его множество раз. Вместо того, чтобы просто написать:
@require_POST
@login_required
@transaction_atomic
def some_view(request):
...
Ответ 2
В дополнение к ответу @Sergey Gornostaev:
Декораторы — по сути реализация аспектно-ориентированного программирования в Питоне.
Вы определяете логику или функциональность отдельно от функций, которые будут пользоваться
этой функциональностью. Возможно, эту логику разрабатывает для вас другая команда.
Пример с логированием, который вы видели практически везде, или те же транзакции —
один из возможных примеров аспекта-функциональности. Затем вы влёгкую можете подключить
использование этого аспекта без ручного переписывания функций, просто добавлением или
убиранием атрибута.
В любом случае, атрибут — часть вашего исходного кода. Если вы хотите, чтобы недекорированная
функция была доступна, вы можете просто не ставить декоратор. Или иметь отдельно недекорированную
функцию, и отдельно её декорированный вариант, который вызывает недекорированный. Или
дописать функциональность, которую вносил декоратор, вручную (но в этом случае вам
нужно будет обновить ваш код, если в декораторе произойдут изменения, например, багфикс;
это обычная проблема дубляжа кода.)
Декоратор просто позволяет вам добавлять функциональность легко.
Ответ 3
# во первых ссылку на функцию можно получить из декоратора
# во вторых теперь эти "куски кода в декораторе" дописать можно к любой функции,
например логирование, проверка аргументов перед вызовом
def decor(fn):
'''декоратор'''
def wrapper(*args, **kwargs):
if all(args):
out = fn(*args, **kwargs)
with open('file.txt', 'a') as log:
log.write('{} {} {} {}'.format(fn, args, kwargs, out))
print('декоратор', end=' ')
return out
return wrapper
@decor
def func(*args):
'''@декоратор(изначальная функция)'''
return sum(args)
func(1, 2, 3) # вызывать как декорируемую
original_fn = func.__closure__[0].cell_contents # func без декоратора
original_fn(1, 2, 3) # вызывать как изначальную функцию
# или
import inspect
original_fn = inspect.getclosurevars(func).nonlocals['fn']
original_fn(0, 2, 3)
# если постоянный вызов дероратора не требуется, изначальную функцию не стоит декорировать
@decor
def func2(*args):
'''изначальная функция'''
return sum(args)
func2(1, 2, 3) # вызывать как изначальную функцию
decor(func2)(1, 2, 3) # вызывать как декорируемую