Оригинал статьи

Автор: Miguel Grinberg

Вольный перевод

Для возвращаемых ответов Flask использует класс под названием Response. Но в самом приложении он редко встречается. Flask оборачивает в него данные ответа как контейнер, при каждом обращении к URL, добавляя необходимую информацию HTTP ответа.

Мало кто знает, что Flask даёт возможность заменить стандартный класс на свой (пользовательский), что позволяет создать более гибкий ответ с переопределённым функционалом. В этой статье я собираюсь показать вам, как воспользоваться преимуществами этой технологии, чтобы упростить код приложения.

Как Flask отдаёт ответы?

Большинство Flask приложений не используют непосредственно класс Response. Но при этом Flask создаёт объект для каждого ответа. Как это работает?

Ответ начинается с того места, когда Flask вызывает функцию обработки запроса. Веб приложения использует маршрут и оканчивает работу вызовом функции render_template, которая вызывает рендеринг файла шаблона и возвращает строку:

@app.route('/index')
def index():
    # ...
    return render_template('index.html')

Но обработчик маршрута Flask не обязан возвращать два значения (код состояния и HTTP заголовки):

@app.route('/data')
def index():
    # ...
    return render_template('data.json'), 201, {'Content-Type': 'application/json'}

Пример выше показывает, как Flask заменяет код ответа 200 по умолчанию на принудительный 201. В примере также установлен параметр Content-Type заголовка ответа, явно указывающий на содержащиеся в ответе данные в формате JSON (по умолчанию Flask возвращает тип HTML).

Приведенные примеры демонстрируют три базовых компонента ответа: данные или тело ответа, код состояния и заголовки. Экземпляр приложения Flask содержит функцию make_response(), которая принимает возвращаемое маршрутом значение (которое, в свою очередь, может быть одним значением или кортежем с одним, двумя или тремя значениями), и создаёт из возвращаемого маршрутом значения объект Response.

Подробности можно увидеть в консоли Python. Создайте виртуальную среду и установите в неё Flask. Затем запустите сессию Python и выполните следующее:

>>> from flask import Flask
>>> app = Flask(__name__)
>>> app.make_response('Hello, World')
<Response 12 bytes [200 OK]>
>>> app.make_response(('Hello, World', 201))
<Response 12 bytes [201 CREATED]>

Здесь я создал экземпляр приложения Flask и вызвал метод make_response(), создавший объект Response. В первом вызове я отправил строку как единственный аргумент, код возврата и заголовки заполняются автоматически значениями по умолчанию. Во втором вызове я отправил кортеж из двух значений, что привело к принудительной установке кода возврата. Обратите внимание на двойные круглые скобки. Это необходимость вызвана тем, что make_response() принимает только один аргумент, и мы отправили кортеж из двух значений (строка и код возврата).

После создания функцией маршрута объекта Response, Flask вызывает обработчик after_request (в числе прочих действий). Обработчик позволяет вставить или изменить заголовки, тело или код возврата. Возможно даже полностью выбросить ответ и/или создать новый. В конце Flask формирует итоговый объект ответа, обрабатывает его как HTTP и отправляет клиенту.

Класс Response

Давайте посмотрим на наиболее интересные аспекты класса ответа. Следующее определение класса показывает характерные атрибуты и методы:

class Response:
    charset = 'utf-8'
    default_status = 200
    default_mimetype = 'text/html'

    def __init__(self, response=None, status=None, headers=None,
                 mimetype=None, content_type=None, direct_passthrough=False):
        pass

    @classmethod
    def force_type(cls, response, environ=None):
        pass

Обращаем Ваше внимание, что в исходном коде Flask Вы не найдёте определений. Класс Response во Flask является небольшой обёрткой вокруг класса Response Werkzeug, который в свою очередь является обёрткой класса BaseResponse (внутри которого и определены элементы).

Три класса атрибутов charset, default_status и default_mimetype определены по умолчанию. При необходимости Вы можете создать собственный класс Response или наследовать имеющийся, и установить необходимые значения для каждого ответа. Для примера рассмотрим реализацию приложения API, которое возвращает XML на все маршруты. Вы можете установить значение default_mimetype в application/xml и Flask будет возвращать XML ответы по умолчанию.

Я не буду вдаваться в подробности описания конструктора __init__ (Вы прочтёте об этом самостоятельно в документации Werkzeug), но обратите внимание на три важных элемента ответов Flask: тело, код возврата и заголовок; которые передаются в качестве аргументов. В подклассе конструктор может переопределить правила формирования ответов.

Метод класса force_type() необычный, но очень важный. Иногда Werkzeug или Flask необходимо создать собственный объект ответа. Например сообщить клиенту о возникновении ошибки в приложении. В этом случае ответ не приходит из приложения. Программная платформа должна создать его (ответ) самостоятельно. В приложении, использующем пользовательский класс ответа, Flask и Werkzeug ничего не знают о деталях класса, поэтому они создают ответ используя стандартный класс. Метод force_type() допускает создание ответа на основе преобразования пользовательского экземпляра класса ответа в собственный формат.

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

Переходя от теории к практике, я покажу как работает Response. Готовы запачкать руки?

Использование собственного класса Response

Я уверен, что есть интересные случаи использования класса ответа. И перед тем, как их показать, рассмотрим настройку приложения Flask c использованием пользовательского класса ответа. Взгляните на следующий пример:

from flask import Flask, Response

class MyResponse(Response):
    pass

app = Flask(__name__)
app.response_class = MyResponse

# ...

Здесь определен пользовательский класс с именем MyResponse. Обычно пользовательский класс добавляет или изменяет поведение по умолчанию, поэтому для создания пользовательских классов используем наследование от класса Response импортированного из модуля Flask. И далее сообщаем приложению Flask использовать наш пользовательский класс, подключив его к app.response_class.

Атрибут response_class класса Flask получает в наш класс ответа в качестве подкласса Flask и позволяет полностью описать наш вариант обработки ответов:

from flask import Flask, Response

class MyResponse(Response):
    pass

class MyFlask(Flask)
    response_class = MyResponse

app = MyFlask(__name__)

# ...

Пример #1: Изменение ответа по умолчанию

Первый пример чрезвычайно прост. Мы указываем приложению возвращать XML для всех ответов. Для этого устанавливаем MIME-тип по умолчанию в application/xml. Буквально две строчки кода:

class MyResponse(Response):
    default_mimetype = 'application/xml'

Легко, не так ли? Пример:

@app.route('/data')
def get_data():
    return '''<?xml version="1.0" encoding="UTF-8"?>
<person>
    <name>John Smith</name>
</person>
'''

Использование пользовательского класса избавляет Вас от необходимости указывать тип возвращаемого ответа каждый раз для каждого маршрута. По умолчанию возвращается тип text/html. В случае необходимости возвращения разных типов для разных маршрутов, потребуется явное указание типа:

@app.route('/')
def index():
    return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}

Пример #2: Автоматическое определение Content-Type

Следующий пример сложнее. Допустим приложение имеет маршруты HTML и XML в равных количествах. Первый пример будет бесполезен, т.к. половина маршрутов будет с неверным типом содержимого.

Лучшим решением является создание класса ответа, который установит корректный тип на основе данных (текста). Рабочий вариант:

class MyResponse(Response):
    def __init__(self, response, **kwargs):
        if 'mimetype' not in kwargs and 'contenttype' not in kwargs:
            if response.startswith('<?xml'):
                kwargs['mimetype'] = 'application/xml'
        return super(MyResponse, self).__init__(response, **kwargs)

В этом примере сначала проверяем, не указан ли MIME-тип явно. А далее примем факт, что XML документ начинается со строки <?xml. Если все условия истинны, я устанавливаю тип XML и отсылаю в конструктор родительского класса.

С помощью этого класса ответа, любой правильный формат XML-документа автоматически получит тип XML, в то время как другие ответы получат тип по умолчанию. И, при необходимости, тип содержимого будет указан явно.

Пример #3: Automatic JSON Responses

Последний пример иллюстрирует то, как обойти проблему при проектировании API интерфейса: вызов в каждом маршруте функции jsonify() для конвертации словаря Python в представление JSON и указание в ответе типа содержимого JSON. Выглядит это так:

@app.route('/data')
def get_data():
    return jsonify({'foo': 'bar'})

Повтор вызова функции jsonify() некорректен как с точки зрения философии Python, так и понижает само качество кода. Как Вам такой вариант?

@app.route('/data')
def get_data():
    return {'foo': 'bar'}

Пользовательский класс ниже поддерживает описанный выше код. При этом не нарушает логику других маршрутов, которые не работают с JSON:

class MyResponse(Response):
    @classmethod
    def force_type(cls, rv, environ=None):
        if isinstance(rv, dict):
            rv = jsonify(rv)
        return super(MyResponse, cls).force_type(rv, environ)

Пояснение: Flask принимает только определённые типы значений, возвращаемых функцией маршрута (str, unikbd, bytes, bytearray), или Вы можете вернуть готовый объект ответа. Flask принимает и понимает такие типы.

Однако в нашем примере возвращается неподдерживаемый тип (словарь). Flask проверяет ответ и для неизвестного типа объекта вызывает метод force_type(), который преобразует неизвестный тип. В нашем примере перегруженный метод применяет необходимые преобразования к неизвестному типу (словарю) и вызывает функцию jsonify().

Такой код не влияет на нормальную отдачу других ответов. Для любых маршрутов, которые возвращают нормальные типы ответа, подкласс ничего не делает, все вызовы передаются прозрачно для родительского класса.

Итог

Надеюсь, что эта статья проливает немного света на работу ответов Flask. Если вы знаете другие варианты формирования ответов Flask — я хотел бы их услышать!

Добавить комментарий