Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 8 из 20 Средний 45 мин 150 XP

Blueprint: организуем большое приложение на Flask

Восьмой урок курса Flask: Blueprint — мини-приложения внутри одного проекта. Регистрируем блюпринты, задаём префиксы адресов, разносим шаблоны по папкам и рефакторим блог из одного файла в модули.

Редакция Питоники

Наш учебный блог незаметно разросся: маршруты постов, комментариев, поиска, админки — всё в одном app.py, и файл перевалил за тысячу строк. Ctrl+F по слову route находит 27 совпадений, и половина из них — не те, что нужны. Переименовали переменную в шаблоне постов — непонятно почему отвалилась форма входа. Знакомо? Тогда у Flask для вас есть подарок из коробки: Blueprint.

Блюпринт — это кусок приложения, упакованный в отдельный модуль: свои маршруты, свои шаблоны, при желании своя статика. Приложение собирается из блюпринтов, как из конструктора: auth отвечает за вход, blog — за посты, api — за данные. В этом уроке разрежем наш блог на модули и заодно разберём грабли, которые на этом пути расставлены щедро: молчаливые 404, конфликты имён эндпоинтов и потерянные шаблоны.

Проблема одного файла: когда app.py перестаёт помещаться в голове

Пока приложение маленькое, один файл — это честно и удобно. Но у монолита из одного файла есть симптомы, которые проявляются рано и болят долго:

  • Прокрутка: чтобы добавить маршрут комментариев, листаешь мимо всех маршрутов постов и авторизации.
  • Конфликты имён: view-функция comments уже занята, новую приходится называть comments_all.
  • Страх рефакторинга: правка в одном месте непредсказуемо ломает другое.
  • Слепые тесты: чтобы проверить один модуль, приходится поднимать всё приложение целиком.

Вот сокращённый в три раза портрет нашего app.py — три разные темы живут вперемешку:

app.py — всё в одном файле (антипример, сокращённо)
from flask import Flask, render_template, request, session, jsonify

app = Flask(__name__)

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

@app.route("/posts")
def posts():
    return render_template("posts.html")

@app.route("/posts/<int:post_id>")
def post_detail(post_id):
    return render_template("post.html", post_id=post_id)

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        session["user"] = request.form["username"]
    return render_template("login.html")

@app.route("/api/posts")
def api_posts():
    return jsonify([{"id": 1, "title": "Первый пост"}])
Серверный код Flask в браузере не запускается — но структура файла передана честно. Дальше разрезаем этот монстр на модули.

Каждый блок здесь тянет свой лист влево-вправо, и от этого код не становится хуже технически — он становится хуже для человека. Через месяц вы не вспомните, на какой строке живёт login. Время резать.

Что такое Blueprint: приложение в миниатюре

Blueprint — объект, который копит маршруты, шаблоны и статику, не привязываясь к приложению. Снаружи он почти неотличим от Flask(__name__): те же декораторы @bp.route, тот же render_template. Разница принципиальна одна: блюпринт сам по себе не запускается. Он оживает только после register_blueprint в основном приложении — тогда Flask переносит все его маршруты в общий роутер.

Имя объекта выбираете вы: чаще всего bp или название модуля. В конструктор Blueprint первым аргументом передают имя — оно станет префиксом для имён эндпоинтов, и к этому мы ещё вернёмся, потому что на именах спотыкаются чаще всего.

blog.py — маршруты блога уезжают в свой модуль
# blog.py
from flask import Blueprint

bp = Blueprint("blog", __name__)

@bp.route("/posts")
def posts():
    return "Список постов"

@bp.route("/posts/<int:post_id>")
def post_detail(post_id):
    return f"Пост номер {post_id}"
Blueprint("blog", __name__): первая строка — имя блюпринта. Оно попадёт в имена эндпоинтов: blog.posts, blog.post_detail.
app.py — регистрируем блюпринт
# app.py
from flask import Flask
from blog import bp as blog_bp

app = Flask(__name__)
app.register_blueprint(blog_bp)

@app.route("/")
def index():
    return "Главная страница"
Строка app.register_blueprint(blog_bp) подключает модуль. Без неё маршруты из blog.py просто не существуют.
Проверяем, что всё работает
$ curl http://127.0.0.1:5000/posts
$ curl http://127.0.0.1:5000/posts/3
Вывод
Список постов
Пост номер 3
Снаружи ничего не изменилось: те же адреса, те же ответы. Разница в том, что внутри проект стал модульным.

Почему это работает: отложенная регистрация

Декоратор @bp.route не вешает маршрут сразу. Он дописывает его во внутренний список блюпринта — отложено. Когда вы вызываете app.register_blueprint(blog_bp), Flask проходит по этому списку и добавляет каждый маршрут в общий роутер приложения. Отсюда приятное следствие: один и тот же блюпринт можно зарегистрировать в двух приложениях — например, в рабочем и в тестовом — и маршруты появятся в обоих.

url_prefix: адресная полка для каждого модуля

Пока маршруты блюпринта живут в корне сайта: /posts, /posts/3. Обычно модулю выделяют префикс — тогда все его адреса начинаются с него, и адресная структура сайта читается сама: /blog/... — посты, /auth/... — вход, /api/... — данные. Префикс задаётся одним аргументом:

Два способа задать url_prefix
# способ 1: префикс при регистрации - карта адресов видна в app.py
app.register_blueprint(blog_bp, url_prefix="/blog")

# способ 2: префикс прямо в конструкторе блюпринта
bp = Blueprint("blog", __name__, url_prefix="/blog")
Результат одинаковый. Я обычно выбираю первый способ: открываю app.py и вижу весь сайт целиком — какой модуль на какой полке.

Теперь список постов отвечает на /blog/posts, а старый адрес /posts отдаёт 404 Not Found — его больше нет. Полный адрес склеивается из префикса и маршрута внутри блюпринта: /blog плюс /posts. Об этом стоит помнить при рефакторинге: ссылки из писем и закладок пользователей по старым адресам сломаются, поэтому префикс лучше заводить сразу, а не когда проект уже индексируется поисковиками.

url_for с блюпринтами: имена эндпоинтов с точкой

У каждого маршрута во Flask есть имя эндпоинта — по умолчанию это имя view-функции. У блюпринтов к имени добавляется префикс: функция posts из блюпринта blog получает эндпоинт blog.posts. Именно это спасает от конфликтов: posts может спокойно жить и в blog, и в api — полные имена разные. В url_for и в шаблонах пишите имя целиком:

Строим адреса через url_for
from flask import url_for

with app.test_request_context():
    print(url_for("index"))
    print(url_for("blog.posts"))
    print(url_for("blog.post_detail", post_id=3))
Вывод
/
/blog/posts
/blog/posts/3
test_request_context позволяет строить URL без поднятого сервера — это же используется в тестах приложений Flask.

В шаблонах вы будете писать то же самое: {{ url_for('blog.post_detail', post_id=post.id) }}. Обратная сторона имён с точкой: напишете url_for("posts"), забыв префикс, — и получите ошибку построения адреса. Хорошая новость в том, что свежие версии werkzeug подсказывают правильное имя прямо в тексте ошибки:

BuildError с подсказкой (werkzeug 3.x)
url_for("posts")

Traceback (most recent call last):
  ...
werkzeug.routing.exceptions.BuildError: Could not build url for endpoint
'posts'. Did you mean 'blog.posts' instead?
Читаем последнюю строку: подсказка Did you mean называет точное имя. В старых версиях werkzeug подсказки нет — тогда помогает app.url_map: распечатайте его и найдите нужный эндпоинт.

Несколько блюпринтов и карта адресов

Реальное приложение — это не один блюпринт, а три-пять. Регистрируются они одинаково, просто по очереди:

app.py — три модуля на одной карте
app.register_blueprint(blog_bp, url_prefix="/blog")
app.register_blueprint(auth_bp)                 # без префикса
app.register_blueprint(api_bp, url_prefix="/api")

print(app.url_map)
Вывод
Map([<Rule '/' (GET, HEAD, OPTIONS) -> index>,
     <Rule '/blog/posts' (GET, HEAD, OPTIONS) -> blog.posts>,
     <Rule '/login' (GET, HEAD, OPTIONS) -> auth.login>,
     <Rule '/api/posts' (GET, HEAD, OPTIONS) -> api.posts>])
app.url_map — честная карта всех адресов приложения: правило, разрешённые методы и полное имя эндпоинта. Когда url_for упрямится, откройте эту карту.

Обратите внимание на круглые скобки в выводе: за каждым адресом стоит список разрешённых HTTP-методов и имя эндпоинта. auth я здесь зарегистрировал без префикса — тогда вход живёт на /login, привычном адресе всех сайтов. Выбор «префикс или нет» — это решение об адресах, которое стоит принимать осознанно: редизайн адресов позже стоит дороже, чем минута размышлений сейчас.

Шаблоны и статика блюпринта: свои папки

У блюпринта могут быть собственные папки шаблонов и статики — они задаются прямо в конструкторе:

Блюпринт со своими папками
bp = Blueprint(
    "blog",
    __name__,
    template_folder="templates",
    static_folder="static",
)
Папки ищутся относительно пакета блюпринта: blog/templates и blog/static. Аппликация Flask работает с ними автоматически.

Теперь Flask ищет шаблон сначала в папке приложения (templates/), потом в папке блюпринта (blog/templates/). Из этого порядка вырастает правило, которое экономит часы отладки: шаблоны блюпринта кладите в подпапку с его именем — blog/templates/blog/post.html, и рендерьте как render_template("blog/post.html"). Если два модуля обзаведутся шаблонами с одинаковым именем post.html, приложение молча возьмёт первый попавшийся — а ошибку вы заметите, когда на странице постов отрисуется форма входа.

Синтаксис самих шаблонов не меняется — переменные, циклы и наследование base.html работают как в уроке про шаблоны Jinja2, просто файлы теперь лежат по своим папкам.

Рефакторинг блога: проект до и после

Собираем всё вместе. Вот структура проекта после разрезания на модули:

Структура проекта с блюпринтами (дерево)
myblog/
  app.py                  # Flask + register_blueprint
  blog/
    __init__.py
    routes.py             # Blueprint("blog")
    templates/
      blog/
        posts.html
        post.html
  auth/
    __init__.py
    routes.py             # Blueprint("auth")
    templates/
      auth/
        login.html
  templates/
    base.html             # общий каркас
    index.html
  static/
    style.css
У каждого модуля — свой пакет со своими маршрутами и шаблонами, общий каркас и статика остаются в корне. Никакой магии: обычные папки и пакеты Python.

Порядок действий при таком рефакторинге я бы предложил такой:

  1. Создайте пакет модуля: папку blog с файлами __init__.py и routes.py.
  2. Перенесите маршруты в routes.py, замените @app.route на @bp.route и объявите Blueprint.
  3. Зарегистрируйте блюпринт в app.py: app.register_blueprint(blog_bp, url_prefix="/blog").
  4. Разложите шаблоны по подпапкам и поправьте render_template: путь теперь blog/post.html.
  5. Проверьте url_for: каждое имя эндпоинта получило префикс blog. — включая ссылки в base.html.

Самый коварный пункт — последний: url_for спрятаны в шаблонах, и BuildError вылезет только на той странице, где он есть. Пройдитесь по сайту руками или держите под рукой общий каркас base.html — обычно именно в нём живёт главное меню со ссылками на все модули. Подробности про общие шаблоны и статические файлы мы разбирали в уроке про структуру проекта.

Типичные ошибки при работе с Blueprint

Ещё одна ловушка из серии «работает, но не так»: циклический импорт. Если blog/routes.py импортирует что-то из app.py, а app.py импортирует блюпринт из blog — Python зациклится с ошибкой ImportError: cannot import name. Лечится разделением: объявление приложения, модели и блюпринты живут в разных файлах, и зависимости идут строго в одну сторону. В больших проектах блюпринты выносят в отдельный пакет с собственным __init__.py, где и создаётся Blueprint.

Когда блюпринт не нужен

Честности ради: если у вас пять маршрутов и один шаблон, блюпринты — оверинжиниринг. Один app.py читается быстрее, чем дерево из папок. Сигналы к разделению: файл перевалил за 300–400 строк, имена функций начали конфликтовать, появился второй смысловой блок (например, к блогу добавили API), или вы хотите переиспользовать модуль в другом проекте. Между «даже не думай» и «рези немедленно» — целая серая зона, и решение в ней всегда за вами: моё правило — резать тогда, когда страница app.py перестала помещаться на экране целиком.

Итоги: из монстра — в конструктор

Теперь у вас есть всё для проекта любого размера: Blueprint объявляется в своём модуле, регистрируется через register_blueprint, адреса модуля собираются под общим url_prefix, а url_for работает с именами вида blog.posts. Шаблоны и статика живут в папках модулей, общие — в корне. Снаружи приложение то же самое, внутри — порядок.

Следующий логичный шаг — самый настоящий модуль auth: в уроке 9 мы построим регистрацию и вход на Flask: модель User, хеширование паролей через werkzeug и декоратор login_required для защиты страниц блога. Он собран на Blueprint из этого урока — так что код auth ляжет в структуру, которую вы только что выстроили. До финала остался один шаг; карта всего курса — самоучитель Flask.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

registered = []

def add_route(target, path):
    target.append(path)

app_routes = []
bp_routes = []

add_route(bp_routes, "/posts")
add_route(bp_routes, "/posts/<int:post_id>")
# блюпринт пока НЕ зарегистрирован в приложении

print(len(app_routes), len(bp_routes))
routes = {
    "index": "/",
    "blog.posts": "/blog/posts",
    "blog.post_detail": "/blog/posts/<int:post_id>",
}

def url_for(endpoint, **kwargs):
    url = routes[endpoint]
    for key, value in kwargs.items():
        url = url.replace("<int:" + key + ">", str(value))
    return url

print(url_for("blog.post_detail", post_id=7))
Проверь себя
0 / 5

1. Что такое Blueprint во Flask?

2. Где можно задать url_prefix для блюпринта?

3. Какое имя эндпоинта у функции posts из блюпринта blog?

4. Что произойдёт, если забыть вызвать app.register_blueprint?

5. Почему шаблоны блюпринта кладут в подпапку blog/templates/blog/?

Карточки терминов
Запомнено: 0 / 6
Практика

Соберите мини-url_for, как во Flask: словарь PREFIXES хранит префиксы блюпринтов (blog -> /blog, auth -> /auth), а ROUTES — шаблоны маршрутов относительно префикса (blog.post_detail -> post_detail/<int:post_id>, auth.login -> login). Функция my_url(endpoint, **kwargs) должна собрать адрес из префикса и маршрута, затем подставить параметры в плейсхолдеры вида <int:post_id>. Выведите два адреса.

practice.py
Вопросы и ответы по уроку

Как посмотреть все маршруты Flask-приложения?

Распечатайте app.url_map: в выводе — все правила приложения с разрешёнными методами и полными именами эндпоинтов, например <Rule '/blog/posts' (GET, HEAD, OPTIONS) -> blog.posts>. Это первый помощник, когда url_for не находит эндпоинт и падает с BuildError.

Что такое Blueprint во Flask и зачем он нужен?

Blueprint — это способ вынести часть приложения (маршруты, шаблоны, статику) в отдельный модуль и зарегистрировать её в основном приложении. Блюпринты решают проблему разросшегося app.py: модули auth, blog и api живут в разных файлах и не конфликтуют именами.

Как разбить Flask-приложение на модули?

Для каждой логической части создайте пакет с файлом routes.py, объявите в нём Blueprint и перенесите маршруты, заменив @app.route на @bp.route. В app.py зарегистрируйте блюпринты с url_prefix, разложите шаблоны по подпапкам и добавьте префиксы к именам эндпоинтов в url_for.

Чем Blueprint отличается от отдельного приложения Flask?

Блюпринт нельзя запустить: у него нет app.run() и собственного сервера. Он получает маршруты, шаблоны и конфигурацию только через родительское приложение. Зато один блюпринт можно подключить к нескольким приложениям — например, к рабочему и к тестовому.

Почему url_for не находит функцию из блюпринта?

У маршрутов блюпринта имена с префиксом: не posts, а blog.posts. Ошибка BuildError с подсказкой «Did you mean 'blog.posts' instead?» значит, что вы написали короткое имя. Распечатайте app.url_map — там перечислены все эндпоинты приложения.

Понравился урок? Сошлитесь на него

«Блюпринт — это кусок приложения, упакованный в отдельный модуль: свои маршруты, свои шаблоны, при желании своя статика.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по Flask

В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по Flask: 20 задач