Прежде всего, стоит отметить, что функции в Python — это first-class objects. Что это означает, можно почитать на странице по ссылке или в другой литературе (неплохая дискуссия по поводу этого термина). На русский язык это переводится с трудом, т.к. слово "первоклассный" у нас имеет несколько другой оттенок, хотя и передает частично то значение, которое подразумевается в данном случае. На практике это означает, что такой объект не имеет ограничений по использованию — он может иметь атрибуты, может быть присвоен переменной, передан в функцию в качестве аргумента, может быть значением возврата из функции и т.п. В отличие от PHP, где передача функции осуществляется при помощи синтаксических хаков с разыменованием переменных, в Python передается непосредственно ссылка на объект-функцию.
При этом, объект функции подчиняется тем же правилам, которым подчиняется любой другой объект в Python — в частности, на него заводится счетчик ссылок, и сам объект не зависит от того первоначального имени, которое было ему дано при определении функции (этому имени может быть впоследствие присвоен другой объект, но объект функции будет доступен по другим ссылкам, если они есть).
Ну и, конечно, функция может быть создана в процессе исполнения другой функции. Все эти особенности и используются для декорирования функций.
Декорирование функций
Функция может быть передана в качестве аргумента в другую функцию, а имени, которое указывает на функцию, можно присвоить другой объект. Это открывает нам широкие возможности писать код в стиле функционального программирования. Мы можем сделать следующее:
[python]
def foo(arg):
print arg
def decorated(fun):
if not hasattr(fun, '__call__'):
raise TypeError ('The argument should be a callable')
def wrapper(arg):
print "calling function %s with arg %s" % (fun.__name__, str(arg))
return fun(arg)
return wrapper
foo = decorated(foo)
foo(1)
[/python]
Результат запуска примера будет выглядеть так:
calling function foo with arg 1
1
Функция
foo
просто выводит переданный ей аргумент. Функция decorated
(позже станет понятно откуда название) принимает в качестве аргумента функцию (точнее, объект, который может быть вызван на исполнение). Если будет передан какой-нибудь неподходящий объект, будет вызвана исключительная ситуация TypeError
. Внутри функции decorated
объявляется функция wrapper
, которая "оборачивает" вызов переданной в decorated
функции, предваряя ее вызов выводом имени вызываемой функции и ее аргумента. Функция wrapper
— фактически шаблон для создания новой функции, которая возникнет только в процессе исполнения decorated
.Эта новая функция — и есть результат вызова
decorated
, и мы присваиваем его имени foo
. После этого foo
ссылается не на оригинальную функцию foo
, а на эту новую функцию, которая была создана из определения wrapper
в процессе исполнения decorated(foo)
. На саму оригинальную foo
осталась ссылка только внутри этой созданной функции.Здесь также использован такой механизм языка, как вложенные области видимости (nested scopes) — если переменная доступна в локальной области видимости функции, то она будет доступна также в локальной области видимости внутри блока другой функции, находящегося в блоке этой функции, если эта переменная там не переопределена. Т.о., аргумент
fun
функции decorated
доступен внутри функции wrapper
, определенной в блоке decorated
.Основное достоинство такого приема в том, что функцию
decorated
можно применять к любой функции, при этом не внося изменений в эти функции. Сделаем что-нибудь более полезное, чем выводить имя функции и аргумент. Например, добавить проверку допустимости аргумента функции.[python]
def require_int(fun):
if not hasattr(fun, '__call__'):
raise TypeError ('The argument should be a callable')
def wrapper(arg):
if not isinstance(arg, int):
raise TypeError ('The argument should be an integer')
return fun(arg)
return wrapper
[/python]
Применив функцию
require_int
к любой функции ( somefunc = require_int(somefunc)
), в которой мы хотим в качестве аргумента видеть только целое число, мы тем самым обеспечим такую проверку, и нам не нужно кодировать такую проверку внутри всех таких функций.Фактически, этот прием — и есть декорирование функций, и доступен в Python задолго до версии 2.4. Функции
decorated
и require_int
— это и есть декораторы. В версии 2.4 лишь появилась специальный синтаксис, упрощающий декорирование.Синтаксис декораторов функций
Синтаксис декорирования был заимствован из синтаксиса аннотаций Java. Для того, чтобы декорировать функцию, нужно перед ее объявлением указать имя функции-декоратора, предваряя его символом
@
. В нашем случае, выражение foo = decorated(foo)
с помощью нового синтаксиса можно записать так:[python]
@decorated
def foo(arg):
print arg
[/python]
В функцию
decorated
никаких изменений вносить не надо.Само собой, мы можем захотеть декорировать нашу функцию несколькими декораторами. Например, нам надо проверять, чтобы аргумент был целым числом, и при этом положительным целым. Мы можем записать это так:
[python]
def require_positive(fun):
if not hasattr(fun, '__call__'):
raise TypeError ('The argument should be a callable')
def wrapper(arg):
if arg <= 0:
raise ValueError ('The argument should be a positive integer')
return fun(arg)
return wrapper
foo = require_positive(foo)
foo = require_int(foo)
[/python]
А можем и так:
[python]
@require_int
@require_positive
def foo(arg):
print arg
[/python]
Обратите внимание на порядок записи декораторов: это выражение эквивалентно
foo = require_int(require_positive(foo))
. Это важно, т.к. порядок применения декораторов часто имеет значение. В нашем случае сначала мы должны проверить целочисленность аргумента, т.е. функция, созданная в require_int
должна быть вызвана прежде, чем функция, созданная в require_positive
проверит, положительное ли число мы получили в аргументе.Как мы видим, при декорировании без использования специального синтаксиса выражения пишутся в обратном порядке —
foo
будет ссылаться на результат последнего выражения, т.е. require_int
отработает первым. Т.о., специальный синтаксис также более удобен, т.к. мы перечисляем декораторы в прямом порядке.Кроме того, такой синтаксис позволяет передачу декоратору аргументов. Например, мы хотим, чтобы аргумент был не менее какого-либо числа.
[python]
@require_int
@require_minimum(20)
def foo(arg):
print arg
[/python]
Стоп. Декоратор у нас принимает только один аргумент - функцию, которую мы декорируем. Каким образом нам написать такой декоратор, который принимает другие аргументы? Приведенное выше выражение эквивалентно такой записи:
foo = require_int(require_minimum(20)(foo))
. Т.е. декоратор здесь - не сама функция require_minimum
, а результат ее вызова с аргументом minimum
, который должен быть функцией, принимающей в качестве аргумента другую функцию. Напишем эту матрешку.[python]
def require_minimum(minimum):
def decorator(fun):
def wrapper(arg):
if arg < minimum:
raise ValueError ('The argument should not be less than %s' % str(minimum))
return fun(arg)
return wrapper
return decorator
[/python]
Функция
require_minimum
содержит определение функции decorator
, которое, в свою очередь, содержит определение функции wrapper
. У нас добавилась еще одна функция — decorator
, которая принимает в качестве аргумента функцию, и из которой, в сущности, и будет создана функция-декоратор. Функция require_minimum
оборачивает ее с той целью чтобы значение минимума передалось декоратору через nested scopes.Само собой,
require_minimum
- это обычная функция, и может быть использована для создания специализированных декораторов, которые уже не потребуют аргумента minimum
, а будут проверять минимальное значение в соответствие с переданным require_minimum
аргументом.Например, наш декоратор
require_positive
- это частный случай require_minimum
, соответственно, для того, чтобы получить эквивалентный по функциональности декоратор, мы можем сделать так:[python]
require_positive = require_minimum(1)
[/python]
Как верно замечает Макс Ищенко, декоратором может служить любой объект, который можно вызвать на исполнение. Такой объект можно сконструитовать, определив в его классе метод
__call__
. Таким образом, require_minimum
можно записать и так:[python]
class require_minimum(object):
def __init__(self, minimum):
self.minimum = minimum
def __call__(self, fun):
def wrapper(arg):
if arg < self.minimum:
raise ValueError ('The argument should be not less than %s' % str(self.minimum))
return fun(arg)
return wrapper
[/python]
В этом случае аргумент
minimum
передается не функции-"матрешке", а конструктору класса. Получившийся объект будет иметь метод __call__
, который и будет нашим декоратором.Применение декораторов
Декораторы применяются достаточно широко. Сфера применения - проверка допустимости аргументов, перехват и изменение аругментов функций, перехват и изменение значений возврата функций, проверка дополнительных условий и т.п. Рассмотрим несколько примеров.
classmethod, staticmethod
При объявлении методов классов поведением по умолчанию будет оборачивание функций в объекты методов, которые при вызове вызывают исходные функции, передавая в качестве первого аргумента экземпляр класса. Подробнее об этом можно почитать у меня в статье про дескрипторы.
Это поведение можно переопределить — как и почти все в Python :)
Если объявление метода предварить декоратором
classmethod
, то будет создан объект-метод, который в первом аргументе функции будет передавать не экземпляр класса, а сам класс.[python]
class X(object):
@classmethod
def myClassMethod(cls):
print cls
[/python]
При вызове метода
myClassMethod
как через сам класс X
, так и через экземпляр этого класса, в переменная cls
будет ссылаться на класс X
.Если объявление метода предварить декоратором
staticmethod
, то будет создан объект-метод, который никак не будет изменять список аргументов, переданный методу. Метод будет вызван как будто это обычная функция, не имеющая представления о состоянии класса или экземпляра класса.Django: проверка аутентификации
Для проверки аутентификации в Django есть удобный декоратор
login_required
. Если мы хотим, чтобы функция-view выполнилась только в том случае, если пользователь прошел процедуру аутентификации, достаточно задекорировать наш view при помощи login_required
. Если пользователь не аутентифицирован, то он будет перенаправлен на страницу логина.[python]
from django.contrib.auth.decorators import login_required
@login_required
def my_view(request):
# do something
[/python]
Декоратор
login_required
определен следующим образом:[python]
def user_passes_test(test_func, login_url=LOGIN_URL):
def _dec(view_func):
def _checklogin(request, *args, **kwargs):
if test_func(request.user):
return view_func(request, *args, **kwargs)
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, quote(request.get_full_path())))
_checklogin.__doc__ = view_func.__doc__
_checklogin.__dict__ = view_func.__dict__
return _checklogin
return _dec
login_required = user_passes_test(lambda u: u.is_authenticated())
[/python]
В данном случае функция
_dec
внутри user_passes_test
по назначению соответствует функции decorator
, а функция _checklogin
— wrapper
в нашей require_minimum
. Функция _checklogin
оборачивает наш view и вызывает его только если переданная user_passes_test
тестовая функция возвращает истинное значение, в противном случае производится редирект.Заметим, что атрибутам функции
_checklogin
присваиваются атрибуты исходного view, т.к. место view занимает уже другая функция. Если этого не делать, арибуты исходной функции будут недоступны.Django: управление транзакциями в базе данных
Для управления транзакциями в Django существует несколько декораторов. Рассмотрим декоратор
commit_on_success
.[python]
from django.db.transaction import commit_on_success
@commit_on_success
def my_func():
# do something
[/python]
Код функции:
[python]
def commit_on_success(func):
def _commit_on_success(*args, **kw):
try:
enter_transaction_management()
managed(True)
try:
res = func(*args, **kw)
except Exception, e:
if is_dirty():
rollback()
raise
else:
if is_dirty():
commit()
return res
finally:
leave_transaction_management()
return _commit_on_success
[/python]
Декоратор
commit_on_success
начинает транзакцию перед выполнением исходной функции, если транзакция еще не начата, и выполняет функцию в блоке try ... except
. В случае возникновения в функции исключительной ситуации транзакция откатывается. В случае успешного завершения функции (критерий успешности — отсутствие исключительных ситуаций) транзакция фиксируется.Оригинальный PEP про декораторы функций и методов.
1. Зачем hasattr(fun...) если есть callable(fun)?
ОтветитьУдалить2. require_minimum можно сделать и классом с методом __call__
1. Исключительно ради того, чтобы показать, что присутствие метода __call__ делает объект callable. На всякий случай, для того, кто не знал.
ОтветитьУдалить2. Да, то что декоратором может выступать любой callable, я хотел написать, но забыл. Спасибо, добавлю.
[...] Прослойка, middleware - самое интересное. Middleware "работает" в обе стороны. Т.е. у нее входной и выходной интерфейс идентичны. Я бы провел аналогию с декоратором. Middleware добавляет некую функциональность в исходное веб-приложение, например live debug, или http auth. Причем, можно выстраивать цепочки middleware. [...]
ОтветитьУдалить[...] Для тех, кто знает, как работают декораторы, должно быть всё понятно, для тех, кто не знает, Максим Деркачев уже все написал . На декоратор append_doc_string и аргумент doc_string внимания пока не обращайте, об этом далее. Пример использования, допустим нам нужен декоратор, который проверяет, является ли аргумент целым числом: [...]
ОтветитьУдалитьОчень хорошая и подробная статья. Читал два дня. :) Как мне кажется, есть много общего между декорированием функций и middleware во WSGI, либо же фильтрацией запроса к базе средствами SQLAlchemy. Так что я для себя извлек максимум пользы. Спасибо.
ОтветитьУдалить