Декораторы
Декораторы используются для оборачивания функций в целях изменения их поведения.
Ключевой особенностью декоратора является возможность принимать функции и возвращать функции. Функция, возвращённая декоратором, будет вызвана в момент вызова декорируемой функции. Следует удостовериться, что оригинальная функция не потерялась в процессе обработки декоратором, так как при ошибке её никак обратно не вернуть без перезагрузки модуля.
Декораторы могут применяться различными способами, как к только что определённой функции, так и к любой другой.
>>> def decorate(func):
... print u"Декорируем %s..." % func.__name__,
... def wrapped(*args, **kwargs):
... print "Вызываем обёрнутую функцию с аргументами:", args
... return func(*args, **kwargs)
... print u"выполнено!"
... return wrapped
...
>>> @decorate
... def test(a, b):
... return a + b
...
Декорируем test... выполнено!
>>> test(13, 72)
Вызываем обёрнутую функцию с аргументами: (13, 72)
85
>>>
Так использовали декораторы до Python 2.4:
>>> def test(a, b):
... return a + b
...
>>> test = decorate(test)
Декорируем test... выполнено!
>>> test(13, 72)
Вызываем обёрнутую функцию с аргументами: (13, 72)
85
Старый подход следует использовать при декорировании функций, которые были определены в других модулях.
Декораторы с параметрами
Временами требуется передать в декоратор дополнительную информацию для управления его поведением. При использовании старого синтаксиса для декоратора или при декорировании определённых функций эту задачу можно решить довольно просто.
>>> def decorate(func, prefix=u"Декорирован"):
... def wrapped(*args, **kwargs):
... return u"%s: %s" % (prefix, func(*args, **kwargs))
... return wrapped
...
>>> simple = decorate(test)
>>> customized = decorate(test, prefix=u"Другое")
>>> print simple(30, 5)
Вызываем обёрнутую функцию с аргументами: (30, 5)
Декорирован: 35
>>> print customized(27, 15)
Вызываем обёрнутую функцию с аргументами: (27, 15)
Другое: 42
>>>
Но новый синтаксис декораторов всё усложняет. При использовании нового синтаксиса декоратор всегда получает только один аргумент: оборачиваемую функцию. Существует метод передачи дополнительных параметров в декораторы, но сначала мы разберёмся с partials
.
Частичное выполнение функций
Обычно функции вызываются с полным набором необходимых аргументов в нужный момент времени. Тем не менее, временами, аргументы могут быть вычислены сильно раньше, чем потребуется вызвать функцию. В этих случаях, к функции можно заранее применить один или более известных аргументов, что затем позволит вызвать эту функцию с меньшим количеством аргументов.
Для этого в Python 2.5 был включен объект partial
в модуль functools
. Он принимает функцию и произвольное количество аргументов, возвращая новую функцию, которая будет работать как оригинальная, требуя лишь предоставить оставшиеся параметры для оригинальной функции.
>>> import functools
>>> def add(a, b):
... return a + b
...
>>> add(4, 2)
6
>>> plus3 = functools.partial(add, 3)
>>> plus5 = functools.partial(add, 5)
>>> plus3(4)
7
>>> plus3(7)
10
>>> plus5(10)
15
>>>
Для версий Python 2.6+ Django предоставляет собственную реализацию partial
в функции carry
, которая определена в модуле django.utils.functional
.
Вернёмся к декораторам
Как было сказано ранее, новый синтаксис декораторов создаёт проблему при необходимости передать декоратору дополнительные параметры. Применяя методику частичного применения возможно предварительное назначение аргументов даже декоратору. Используя декоратор, определённый ранее, следующий пример использует curry
для передачи аргументов в декоратор:
>>> from django.utils.functional import curry
>>> @curry(decorate, prefix=u"Окрутили")
... def test(a, b):
... return a + b
...
>>> print test(30, 5)
Окрутили: 35
>>>
Этот метод всё равно неудобен, так как функция должна запускаться через curry
при каждом её использовании для декорирования других функций. Лучше будет реализовать подобный функционал в самом декораторе.
>>> def decorate(prefix=u"Декорирован"):
... # Префикс, указанный здесь, будет доступен всем вложенным
... # функциям.
... def decorator(func):
... def wrapper(*args, **kwargs):
... # Эта функция будет вызываться при каждом вызове
... # декорируемой функции.
... return u"%s: %s" % (prefix, func(*args, **kwargs))
... return wrapper
... return decorator
...
>>> @decorate(u"Просто")
... def test(a, b):
... return a + b
...
>>> print test(13, 17)
Просто: 30
>>>
Данная методика позволяет обработать большинство ситуаций, когда требуется передача дополнительных аргументов в декоратор. Тем не менее, наличие скобок обязательно, даже если в декоратор не передаются дополнительные параметры.
>>> @decorate()
... def test(a, b):
... return a + b
...
>>> print test(13, 17)
Декорирован: 30
>>>
>>> @decorate
... def test(a, b):
... return a + b
...
>>> print test(13, 17)
Traceback (most recent call last):
File "<console>", line 1, in <module>
TypeError: decorator() takes exactly 1 argument (2 given)
>>>
Второй пример завершился с ошибкой, потому что мы не вызвали сначала decorate
. Следовательно, все последующие вызовы test
отправляли свои аргументы в декоратор, а не в декорируемую функцию. Из-за этого несовпадения Python выкидывает исключение. Въехать в проблему может быть непросто, так как тип исключения, которое будет вызвано, будет зависеть от декорируемой функции.
Декоратор с и без аргументов
Попробуем сделать декоратор, который сможет работать как с дополнительными аргументами, так и без них. Следовательно, все аргументы декоратора будут необязательными. Учитывая это, основной идеей будет добавление дополнительного необязательного аргумента в начало списка, который получит декорируемая функция. Далее, структура декоратора включает в себя необходимую логику для определения режима работы: вызван декоратор для добавления аргумента или для декорирования функции.
>>> def decorate(func=None, prefix=u"Декорирован"):
... def decorated(func):
... # Эта функция является финальной, декорированной функцией,
... # независимо от способа её вызова.
... def wrapper(*args, **kwargs):
... return u"%s: %s" % (prefix, func(*args, **kwargs))
... return wrapper
... if func is None:
... # Декоратор был вызван с аргументами.
... def decorator(func):
... return decorated(func)
... return decorator
... # Декоратор был вызван без аргументов.
... return decorated(func)
...
>>> @decorate
... def test(a, b):
... return a + b
...
>>> print test(13, 17)
Декорирован: 30
>>>
>>> @decorate(prefix=u"Аргументы")
... def test(a, b):
... return a + b
...
>>> print test(13, 17)
Аргументы: 30
>>>
Приведённый код требует, чтобы декоратору передавались только именованные аргументы, это делает код более читаемым. Но делать подобное для каждого декоратора устанешь.
К счастью, есть возможность упростить код, создав ещё один декоратор. Следующая функция может быть использована для декорирования других функций, предоставляя функциональность для обработки аргументов, если они указаны.
>>> def optional_argument_decorator(real_decorator):
... def decorator(func=None, **kwargs):
... # Этот декоратор будет использоваться в вашей программе.
... def decorated(func):
... # Эта функция является финальной, декорированной функцией,
... # независимо от способа её вызова.
... def wrapper(*a, **kw):
... return real_decorator(func, a, kw, **kwargs)
... return wrapper
... if func is None:
... # Декоратору переданы аргументы.
... def decorator(func):
... return decorated(func)
... return decorator
... # Декоратор был вызван без аргументов.
... return decorated(func)
... return decorator
...
>>> @optional_argument_decorator
... def decorate(func, args, kwargs, prefix=u"Декорирован"):
... return u"%s: %s" % (prefix, func(*args, **kwargs))
...
>>> @decorate
... def test(a, b):
... return a + b
...
>>> print test(13, 17)
Декорирован: 30
>>> test = decorate(test, prefix=u"Декорирован ещё")
>>> print test(13, 17)
Декорирован ещё: Декорирован: 30
>>>
Такая методика упрощает определение отдельных декораторов. Результирующий декоратор работает так же, как и представленный ранее, но может обрабатывать ноль или более необязательных аргументов. Отличие заключается в том, что эта методика требует декоратор, который обязательно принимает следующие три значения:
func
— функция, которая будет декорирована сгенерированным декоратором;args
— кортеж неименованных аргументов, которые будут переданы функции;kwargs
— словарь именованных аргументов, которые будут переданы функции.
Важно отметить, что параметры args
и kwargs
указываются без использования «звёздочек». Затем, при передаче их декорированной функции, должны использоваться «звёздочки», чтобы функция принимала аргументы, не зная об устройстве декоратора.