Class-based views вглубь

Разбираемся, как устроены class-based views и почему они экономят код в типовых сценариях.

Class-based view (CBV) — это представление, оформленное как класс: HTTP-методы становятся методами объекта (get, post), а повторяющуюся логику собирают готовые базовые классы и миксины.

Зачем это нужно на практике

В реальных проектах десятки страниц делают одно и то же: показать список объектов с пагинацией, открыть карточку одного объекта, отрисовать форму создания и сохранить её. Если писать это функциями, у вас накапливается копипаста: запрос к базе, проверка 404, рендер шаблона, передача контекста — и так в каждом view. CBV выносят этот скелет в базовые классы, а вам остаётся указать только отличия: модель, шаблон, поля. Представьте интернет-магазин: каталог товаров, карточка товара, список заказов, карточка заказа, профиль, список отзывов — все эти страницы по структуре близнецы. На функциях получается шесть почти одинаковых блоков по тридцать строк; на CBV — шесть классов по пять строк.

Главная выгода — не «классы вместо функций ради красоты», а переиспользование через наследование и миксины. Один раз написали миксин «доступ только авторизованным» — и подмешиваете его в любой view. Завели базовый класс с общим оформлением списков — и наследуете от него все страницы-каталоги, переопределяя одну-две строки. Поэтому CBV особенно хороши там, где много однотипного CRUD: чем больше в проекте похожих страниц, тем сильнее они окупаются. На совсем маленьком проекте из трёх разных view выгода почти нулевая — и это нормально, инструмент рассчитан на масштаб.

Иерархия и миксины

В основе всего лежит View — он умеет принимать запрос и направлять его в метод по имени HTTP-глагола. Над ним надстроены готовые «обобщённые» (generic) представления. Логика разнесена по маленьким классам-миксинам, каждый отвечает за свой кусочек:

Класс/миксинЗа что отвечает
Viewбазовая диспетчеризация запроса по HTTP-методу
TemplateResponseMixinзнает, как отрендерить шаблон в ответ
ContextMixinсобирает словарь контекста (get_context_data)
SingleObjectMixinдостаёт один объект по pk или slug
MultipleObjectMixinдостаёт список объектов и делает пагинацию

Готовые представления — это просто удачные комбинации этих кирпичиков. DetailView = «достать один объект» + «отрендерить шаблон». ListView = «достать список с пагинацией» + «отрендерить шаблон». Понимание этой сборки важнее, чем зубрёжка: когда что-то идёт не так, вы знаете, какой миксин за это отвечает.

ListView, DetailView, CreateView

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

from django.views.generic import ListView, DetailView, CreateView
from .models import Article

class ArticleList(ListView):
    model = Article
    paginate_by = 20            # пагинация из коробки
    # шаблон по умолчанию: article_list.html
    # объект в контексте: object_list / article_list

class ArticleDetail(DetailView):
    model = Article
    # ждёт pk или slug в URL, отдаёт 404 если нет
    # шаблон: article_detail.html, объект: object / article

class ArticleCreate(CreateView):
    model = Article
    fields = ["title", "body"]   # ModelForm соберётся автоматически
    success_url = "/articles/"   # куда уйти после сохранения

Обратите внимание: ListView сам кладёт в контекст paginator и page_obj, CreateView сам строит ModelForm по списку fields и при POST валидирует и сохраняет её. Подключаются они в urls.py через .as_view():

path("articles/", ArticleList.as_view()),
path("articles/<int:pk>/", ArticleDetail.as_view()),

get_queryset и get_context_data

Атрибут model = Article — это удобное умолчание, эквивалент Article.objects.all(). Но в живом приложении «показать вообще всё» почти никогда не нужно: список фильтруют по статусу, сортируют, ограничивают правами доступа, а на странице рядом со списком обычно есть боковые блоки. Как только нужна фильтрация, сортировка или дополнительные данные, атрибута уже мало — переопределяют методы. Это две главные «ручки» настройки любого generic-view:

class PublishedArticleList(ListView):
    template_name = "articles/list.html"
    paginate_by = 20

    def get_queryset(self):
        # вместо всех — только опубликованные, свежие сверху
        qs = Article.objects.filter(is_published=True).order_by("-created")
        author = self.request.GET.get("author")
        if author:
            qs = qs.filter(author__username=author)
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)   # не забыть super()!
        ctx["total"] = self.get_queryset().count()
        ctx["popular"] = Article.objects.order_by("-views")[:5]
        return ctx

Запомните разделение ролей: get_queryset отвечает за то, какие объекты показать (фильтры, сортировка, права доступа), а get_context_data — за всё остальное, что нужно шаблону (счётчики, боковые блоки, формы). В обоих внутри доступен self.request, а значит и пользователь, и GET-параметры.

Как это работает под капотом

Когда в URL стоит ArticleList.as_view(), Django не создаёт объект сразу. as_view() возвращает обычную функцию-обёртку. На каждый запрос эта обёртка создаёт новый экземпляр класса, кладёт в него self.request, self.args, self.kwargs и вызывает dispatch(). А dispatch() смотрит на request.method и вызывает одноимённый метод: для GET-запроса — self.get(), для POST — self.post().

Из этого следует важное правило: на каждый запрос — свой объект view, поэтому хранить состояние между запросами в атрибутах экземпляра безопасно, а вот в атрибутах класса (как model, fields) — это общие на всех настройки, менять их в рантайме нельзя. Цепочка вызова метода get() у ListView такая: получить queryset → применить пагинацию → собрать контекст → отрендерить шаблон. Каждый шаг — отдельный переопределяемый метод, отсюда гибкость.

Когда CBV, а когда функции

CBV — не всегда лучший выбор. Ориентир простой:

  • Берите CBV, когда задача ложится на готовый паттерн: список, карточка, форма создания/редактирования/удаления. Тут вы пишете 5 строк вместо 30 и бесплатно получаете пагинацию, 404 и сборку формы.
  • Берите функцию (FBV), когда логика своя и нелинейная: сложное ветвление, несколько форм на одной странице, хитрая обработка платежа, приём вебхука от внешнего сервиса. В функции весь поток виден сверху вниз, его проще читать пошагово и отлаживать — поставил print или брейкпоинт и идёшь по строкам.

Антипаттерн — тащить generic-view туда, где переопределены почти все методы: получается «функция, вывернутая наизнанку через наследование», читать её тяжелее, чем обычную FBV, потому что управление прыгает между вашими методами и кодом базовых классов. Если от ListView осталось только имя, а тело целиком переписано — честнее написать функцию. Хороший признак здоровой CBV: вы переопределяете один-два метода и опираетесь на остальные, а не воюете с фреймворком. И ещё ориентир из практики: команды редко смешивают стили без причины — если проект исторически на функциях, новую типовую страницу часто проще сделать тем же стилем, чем разводить зоопарк.

Частые ошибки

  • Забыли super() в get_context_data. Если вернуть свой словарь без вызова родителя, из контекста пропадут object_list, page_obj и форма — шаблон «поедет».
  • Тяжёлый запрос дважды. Вызов self.get_queryset() внутри get_context_data повторно бьёт в базу. Для подсчёта используйте context["paginator"].count или сохраняйте queryset.
  • Логика в __init__. На момент конструктора ещё нет self.request. Любую работу с запросом делайте в get/get_queryset/dispatch, а не в __init__.
  • Слепое наследование. Переопределяя метод, всегда сверяйтесь с тем, что делал базовый: легко случайно «отkey» пагинацию или 404-проверку.

Итоги

  • CBV собирают типовые view из миксинов; выгода — переиспользование, а не классы ради классов.
  • ListView/DetailView/CreateView закрывают большую часть CRUD почти без кода.
  • get_queryset решает «что показать», get_context_data — «что ещё положить в шаблон»; в get_context_data обязателен super().
  • as_view() на каждый запрос создаёт новый объект и через dispatch зовёт метод по HTTP-глаголу.
  • Линейную нестандартную логику честнее оставить функцией, чем выкручивать через generic-view.
Проверьте себя
1. Что произойдёт, если в get_context_data вернуть свой словарь, не вызвав super().get_context_data()?
AНичего, контекст соберётся как обычно
BИз контекста пропадут object_list, page_obj и форма, которые добавляют родительские классы
CDjango выбросит исключение TypeError
DШаблон отрендерится, но без CSRF-токена
2. За что отвечает метод get_queryset в ListView?
AЗа выбор и формирование набора объектов: фильтры, сортировку, права доступа
BЗа рендеринг шаблона в HTTP-ответ
CЗа добавление в контекст вспомогательных данных вроде счётчиков
DЗа определение, какой HTTP-метод вызвать
3. Когда уместнее обычная view-функция, а не class-based view?
AКогда нужна пагинация списка объектов
BКогда нужно показать карточку одного объекта с проверкой 404
CКогда логика нестандартная и линейная, и в generic-view пришлось бы переопределить почти все методы
DНикогда — CBV всегда предпочтительнее функций