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.