Armin Ronacher: Flask for Fun and Profit

Mikhail Novikov
Nov 1 · 8 min read

За что любят Flask

Flask основан на werkzeug (читается: вёркцойг). Это маленькая библиотека, реализующая стандарт WSGI, которая живет посередине между WSGI-сервером, принимающим HTTP-запросы, и самим веб-приложением. Стандарт WSGI появился в 2004 году, и он до сих пор является основой веб-приложений на питоне. Философия werkzeug, а впоследствии и Flask, — в том, что пользователю нужно дать как можно больше свободы и не навязывать ему архитектурные решения. И именно в этом залог его популярности: с Flask стартовать проще всего. На нем легко писать приложения, рендерящие HTML; на нем еще легче делать REST-микросервисы.

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

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

Создание приложения

Самый удобный способ — создать приложение фабрикой create_app. Это позволит легко и понятно управлять конфигурацией; например, одной фабрикой создавать боевую версию аппы, а другой фабрикой — создавать аппу для тестов и по-своему её конфигурировать. Кроме того, если вы запускаете тесты в несколько потоков, фабрика create_app избавит ваши потоки от общего глобального объекта app.

Сравните это с django, где все модули смотрят на глобальный конфигурационный файл и на его переменные. Django не так-то просто переконфигурировать на лету.

Если вам все же нужен глобальный объект — можно создать пустую аппу глобально и сконфигурировать её функцией register_blueprints(), которая повесит на неё все нужные роуты и логику.

Ещё один интересный способ создать приложение — это завернуть его в новый объект, который будет все вызовы передавать в приложение. Это удобно для изоляции неймспейсов.

Зачем это нужно? Например, если вы пишете на базе Flask какое-то приложение и хотите повесить на него логику, не связанную собственно с Flask или с вебом (к примеру, ваше приложение должно слушать redis или rabbitmq).

Завернув приложение в отдельный объект, вы изолируете функционал, уменьшите сопряжение объектов и не перезапишете случайно какие-то из существующих атрибутов (а такой риск существует: на объекте app висит очень много атрибутов).

Дев-север

Завернув создание приложения в функцию (или в объект), мы должны решить, где нам все-таки вызвать эту функцию, чтобы запустить дев-сервер. Традиционно для этого создается отдельный файл, в который импортируется эта функция и вызывается:

И после этого командой flask run запускается этот файл:

Многие вместо flask run кладут в конец файла команду app.run() . У этого подхода есть два недостатка.

  1. Во-первых, если вы используете релоадер, то, встретив в вашем коде ошибку SyntaxError, он сломается и не перезапустится. Команда flask run страхует нас от этой проблемы.
  2. Во-вторых, в варианте app.run() , когда мы запускаем приложение, релоадер проходит по исполняемому файлу два раза — и дважды прогоняет весь код с начала до конца. И если перед app.run() у вас в коде создается БД, то релоадер создаст вам эту БД дважды.

Очень интересный момент — Debugger PIN. Дело в том, что страницы с ошибками, которые генерит Flask, — интерактивны. Можно отлаживать код прямо в них, внутри трейса. Соответственно, PIN нужен для защиты, если вдруг кто-то наткнется на вашу дебаг страницу и решит её хакнуть.

Глобальные объекты

Главный холивар Фласка идет вокруг глобальных объектов current_app, request и g. В django такого нет; людям, привыкшим к django, глобальные объекты кажутся странными.

Ирония в том, что в django глобалки тоже есть. Например, как, по-вашему, функция gettext() определяет, на какой язык переводить ту или иную фразу? Она берет это из секретного глобального объекта, содержащего данные о запросе из браузера. Flask не делает из глобалок секрет; он показывает вещи такими, какие они есть на самом деле.

Два глобальных контекста

Во фласке есть два глобальных контекста. Один известен всем — это контекст запроса, request и session. О втором знают немногие: это контекст приложения, current_app и g . Контекст приложения, точно так же, как и контекст запроса, сбрасывается после ответа на запрос.

Зачем тогда нужен контекст приложения, если он сбрасывается точно так же? Он нужен в ситуации, когда у нас просто нет такой вещи, как HTTP-запрос. Например, когда мы запускаем cron-джоб по графику, а внутри джоба хотим запустить функцию, требующую контекст приложения (язык пользователя, какие-то глобальные настройки и тд).

Пример работы с контекстом приложения

При создании контекста g кладем туда настройки (коннект к БД, язык пользователя и тп). Дальше, в коде функций, достаем из контекста g нужные данные по необходимости.

Вот пример, как положить в контекст подключение к БД и закрыть его при завершении контекста. Django делает это под капотом; Flask предлагает вам сделать это явным образом (и многие расширения Flask этим пользуются).

Еще пример: функцию получения пользователя можно сначала направить на g, а если его там нет — то на request. Это очень удобно для тестов: контекст g создать и убить гораздо дешевле, чем контекст request.

JSON API

Есть море библиотек для JSON API поверх Flask. Но я, честно говоря, их не использую и пишу всё сам. Почему так?

Во-первых, фреймворки навязывают нам собственное представление об API: как делать пагинацию, какой mimetype ставить в ответе и т.д. Установив фреймворк, мы в какой-то момент начинаем с ним бороться и прогибать под свою логику. Или прогибаемся сами.

Во-вторых, для создания JSON API нужно на самом деле очень мало кода. Я обычно делаю объект ApiResult, который умеет раскрываться в Response с нужным статусом и хедерами. Такой подход очень хорошо расширяется. Например, сюда можно добавить пагинацию в нужном формате, обработку пробелов и табов, и тд.

Чтобы научить вьюхи правильно пониматьApiResult, нужно унаследоваться от Flask и переопределить make_response(). И всё.

Если нам нужно добавить что-то в наш API, например, хедеры пагинации, — с ApiResult это делается элементарно. А вот если бы мы взяли фреймворк, то нам пришлось бы с ним воевать, чтобы добиться нужного формата пагинации.

Как быть с исключениями? Делаем класс ApiException, который умеет генерировать ответы с кодом, например, 400, и регистрируем на приложении хендлер для этого класса. Теперь можно делать во вьюхах raise ApiException , и Flask превратит это в корректный JSON-ответ с правильным кодом.

Валидация и сериализация

Большинство библиотек для валидации и сериализации в питоне впадают в одну из двух крайностей:

  • либо у них прикольный простой API, но куча подкапотной магии, которая рано или поздно начинает вас ограничивать;
  • либо они могут всё — и с ними невозможно справиться.

Есть компромисс: jsonschema , кроссплатформенный стандарт валидации. Многие пользуются именно им. Лично я предпочитаю voluptuous . С его помощью можно настраивать валидацию фласковских вьюх прямо в декораторах вьюх. (обратите внимание на выброс ApiException в глубине декоратора — этот класс мы создали выше)

Безопасность

Самый распространенная дырка в безопасности — это когда мы забываем ограничить выборку данных по ID юзера и вместо этого отдаём данные всех юзеров. Простой способ избежать этого — подружить ваши модели с контекстом выполнения. Я для этого держу в своих проектах модуль security, в котором храню фильтры по данным. После этого достаточно переопределить на модели атрибут query и подложить туда такой фильтр (см. get_available_organizations()). Теперь все данные по запросу Project.query... будут автоматически фильтроваться в зависимости от контекста конкретного запроса.

Экранирование HTML

Все знают, что, когда в контекст шаблона jinja попадает переменная, содержащая HTML, все теги этого HTML автоматически экранируются. Это сделано во избежание XSS-атак: если вы принимаете у пользователей данные и потом рендерите их в шаблонах, то вас могут хакнуть, внедрив вам на страницу вредоносный JS внутри тега <script>. Но немногие знают, что подобная вещь существует и для JSON. Если вы собираетесь отдавать внутри JSON строки, содержащие HTML (да, бывают такие кейсы), попробуйте это:

Тестирование

Лучше всего тестировать Flask-приложения с помощью pytest и его фикстур. Самая простая и полезная фикстура — ниже. Здесь мы создаем приложение, включаем его контекст (ctx.push) и вешаем на момент окончания жизни фикстуры выключение контекста (ctx.pop). Важно: request здесь — это не запрос от фласка, это стандартная фикстура pytest: https://docs.pytest.org/en/latest/fixture.html#request-context

Прим.перев.: вместо неочевидного request.addfinalizer можно так:

...
ctx.push()
yield app
ctx.pop()

На базе этой фикстуры можно сделать фикстуру тестового клиента:

Прим. перев: и здесь можно обойтись без __enter__ и __exit__, написав:

...
with app.test_client() as client:
yield client

Вебсокеты

Фласк не приспособлен для вебсокетов. Он не умеет держать коннекты, он создан для простых HTTP-запросов и HTTP-ответов. Для вебсокетов нужен отдельный сервер.

Даже если попытаться приспособить Flask под вебсокеты (или взять другой фреймворк, который умеет вебсокеты) — всё равно желательно держать для вебсокетов отдельный сервер. Почему? Представьте себе ситуацию:

  • Вы выкатили приложение с вебсокетами. Ваше приложение набрало тысячу подключений от разных браузеров — и держит их.
  • Вы выкатываете новую версию приложения. После перезагрузки сервера все подключения обрываются.
  • После обрыва браузеры одновременно идут восстанавливать подключение — и обрушивают ваше свежезапущенное приложение тысячей запросов. (особенно если вебсокет-подключения требуют работы с БД).

Простейший вебсокет-сервер — это приложение, которое подписывается на redis по pub/sub и отрывает вебсокет-подключение для браузеров. Дальше мы из Flask пушим в redis сообщения, их получает наш вебсокет-сервер и отправляет клиентам. Жаль, что такого вебсокет-сервера нет в опенсорсе. (Прим.перев.: теперь есть! https://github.com/centrifugal/centrifugo)

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade