Декораторы в языке Python являются мощным средством для борьбы с избыточностью. Наряду с компоновкой функциональности в соответствующих небольших методах, они позволяют упаковать сложный процесс в краткий код.
На примере Django, рассмотрим представление, которое обрабатывает запросы, получая объект запроса и возвращая объект отклика:
def handle_request(request):
return HttpResponse(u"Привет всем")
Недавно потребовалось написать несколько методов API, которые должны:
- возвращать JSON в отклике;
- возвращать код ошибки, если запрос не был послан через POST.
В качестве примера, для регистрации клиента API мне пришлось написать следующее:
def register(request):
result = None
# проверяет тип запроса
if request.method != 'POST':
result = {"error": u"функция принимает только POST запросы!"}
else:
try:
user = User.objects.create_user(request.POST['username'],
request.POST['email'],
request.POST['password'])
# необязательные поля
for field in ['first_name', 'last_name']:
if field in request.POST:
setattr(user, field, request.POST[field])
user.save()
result = {"success": True}
except KeyError as e:
result = {"error": str(e) }
response = HttpResponse(json.dumps(result))
if "error" in result:
response.status_code = 500
return response
Однако, мне необходимо отдавать JSON и код ошибки практически в каждом методе API. Показанный выше подход приведёт к многократному воспроизведению этой логики. Давайте последуем принципам DRY, используя декораторы.
Введение в декораторы
Если вы не сталкивались с декораторами, знайте, это эффективные функции-обёртки, которые выполняются когда интерпретатор Python вызывает функцию, и могут модифицировать как значения аргументов функции, так и её результат. Например, если нам надо всегда увеличивать на единицу целое значение, возвращаемое функцией, то можно написать следующий декоратор:
# декоратор получает обёртываемую функцию в виде переменной 'f'
def increment(f):
# такие аргументы обеспечивают захват всех параметров.
def wrapped_f(*args, **kw):
# отметьте, что мы вызываем f, используя переменные, переданные
# в wrapper, и преобразовываем результат в целое, увеличивая его.
return int(f(*args, **kw)) + 1
return wrapped_f # возвращается обёрнутая функция
И теперь мы можем использовать его для декорирования другой функции с помощью символа @
:
@increment
def plus(a, b):
return a + b
result = plus(4, 6)
assert(result == 11, u"Декоратор написан неправильно!")
Декораторы модифицируют существующую функцию, воздействуя на результат её выполнения. В данном случае функция plus
действительно возвращает результат increment(plus(4, 6))
.
Возвращение ошибки для неPOST запросов
Давайте заставим декораторы делать что-то полезное. Пусть декоратор, который возвращает ошибку, если запрос не был отправлен через POST:
def post_only(f):
""" Проверяет метод, должен быть POST """
def wrapped_f(request):
if request.method != "POST":
response = HttpResponse(json.dumps(
{"error": u"разрешены только POST запросы!"}))
response.status_code = 500
return response
return f(request)
return wrapped_f
Теперь мы можем применить его к методу регистрации нашего API:
@post_only
def register(request):
result = None
try:
user = User.objects.create_user(request.POST['username'],
request.POST['email'],
request.POST['password'])
# необязательные поля
for field in ['first_name', 'last_name']:
if field in request.POST:
setattr(user, field, request.POST[field])
user.save()
result = {"success": True}
except KeyError as e:
result = {"error": str(e) }
response = HttpResponse(json.dumps(result))
if "error" in result:
response.status_code = 500
return response
У нас есть декоратор, который мы можем применить ко всем методам нашего API.
Отправка отклика в виде JSON
Для того, чтобы отправить отклик в виде JSON, а также обработать ошибку 500, мы можем создать следующий декоратор:
def json_response(f):
""" Возвращает JSON или 500 статус при наличии ошибки """
def wrapped(*args, **kwargs):
result = f(*args, **kwargs)
response = HttpResponse(json.dumps(result))
if type(result) == dict and 'error' in result:
response.status_code = 500
return response
Теперь мы можем удалить код, относящийся к JSON, из наших методов, добавив декоратор:
@post_only
@json_response
def register(request):
try:
user = User.objects.create_user(request.POST['username'],
request.POST['email'],
request.POST['password'])
# необязательные поля
for field in ['first_name', 'last_name']:
if field in request.POST:
setattr(user, field, request.POST[field])
user.save()
return {"success": True}
except KeyError as e:
return {"error": str(e) }
Теперь, если мне понадобится написать новый метод я смогу просто воспользоваться этими декораторами, чтобы выполнить повторяющуюся задачу. Если мне надо создать метод авторизации, я напишу только необходимый код:
@post_only
@json_response
def login(request):
if request.user is not None:
return {"error": u"Пользователь уже аутентифицирован!"}
user = auth.authenticate(request.POST['username'], request.POST['password'])
if user is not None:
if not user.is_active:
return {"error": u"Пользователь неактивен"}
auth.login(request, user)
return {"success": True, "id": user.pk}
else:
return {"error": u"Такого пользователя не существует"}
Бонус №1: параметризация оборачиваемой функции
Я использовал фреймворк Turbogears и восхищался его интерпретацией параметров запроса и передачей их в метод. Можно ли воспроизвести такое поведение в Django? Да, декоратор -- один из способов этого!
Например:
def parameterize_request(types=("POST",)):
"""
Параметризует запрос вместо прямой его передачи.
Указанные типы будут добавлены к параметрам запроса.
т.е. преобразовывает a=test&b=cv из request.POST в
f(a=test, b=cv)
"""
def wrapper(f):
def wrapped(request):
kw = {}
if "GET" in types:
for k, v in request.GET.items():
kw[k] = v
if "POST" in types:
for k, v in request.POST.items():
kw[k] = v
return f(request, **kw)
return wrapped
return wrapper
Следует отметить, что это пример параметризации декоратора. В данном случае, результатом функции является декоратор.
Теперь, я могу создавать методы с параметризованными аргументами! Я даже могу выбирать, пропускать GET и POST запросы или один из них.
@post_only
@json_response
@parameterize_request(["POST"])
def register(request, username, email, password,
first_name=None, last_name=None):
user = User.objects.create_user(username, email, password)
user.first_name=first_name
user.last_name=last_name
user.save()
return {"success": True}
Теперь у нас есть краткий и понятный API!
Бонус №2: Использование functools.wraps
для сохранение докстринга и имени функции
К сожалению, одним из побочных эффектов использования декораторов является потеря значений имени функции (__name__
) и докстринга (__doc__
):
def increment(f):
""" Увеличивает на единицу результат функции """
wrapped_f(a, b):
return f(a, b) + 1
return wrapped_f
@increment
def plus(a, b)
""" Складывает значения аргументов """
return a + b
plus.__name__ # теперь тут 'wrapped_f' вместо 'plus'
plus.__doc__ # теперь тут 'Увеличивает на единицу результат функции'
# вместо 'Складывает значения аргументов'
Это приводит к проблемам с приложениями, которые используют эту информацию. Например, с Sphinx, инструментом генерации документации по вашему коду.
Для решения этой проблемы вы можете использовать декоратор wraps
:
from functools import wraps
def increment(f):
""" Увеличивает на единицу результат функции """
@wraps(f)
wrapped_f(a, b):
return f(a, b) + 1
return wrapped_f
@increment
def plus(a, b)
""" Складывает значения аргументов """
return a + b
plus.__name__ # здесь будет 'plus'
plus.__doc__ # здесь будет 'Складывает значения аргументов'
Бонус №3: Использование декоратора decorator
Если вы посмотрите на приведённые выше декораторы, то вы заметите немало повторений в их коде. Вы можете установить модуль decorator
, который содержит декоратор decorator
, для избавления от этой рутины.
Варианты установки:
$ sudo easy_install decorator
$ pip install decorator
Затем вы можете просто делать:
from decorator import decorator
@decorator
def post_only(f, request):
""" Проверяет метод, должен быть POST """
if request.method != "POST":
response = HttpResponse(json.dumps(
{"error": u"функция принимает только POST запросы!"}))
response.status_code = 500
return response
return f(request)
Что особо привлекает, этот декоратор сохраняет значения __name__
и __doc__
, т.е. он реализует функционал декоратора wraps