Все представления, которые мы написали за пять модулей, — это функции. В Django их называют FBV (Function Based Views). Но Django предоставляет и альтернативный подход — CBV (Class Based Views), где представление описывается классом.
Это не новая концепция, а другой способ организации того же самого кода. Оба подхода полностью взаимозаменяемы — Django одинаково хорошо работает и с функциями, и с классами в urls.py.
Рассмотрим конкретную проблему. У нас уже есть несколько похожих представлений:
def film_list(request):
films = Film.objects.select_related('director').prefetch_related('genres')
return render(request, 'films/film_list.html', {'films': films})
def about(request):
return render(request, 'films/about.html', {'title': 'О нашем сайте'})Оба представления делают одно и то же по структуре: получить данные, отрендерить шаблон. Если в проекте десятки таких простых страниц — код повторяется. CBV даёт способ выразить эту структуру через наследование, вынося общую логику в базовые классы, которые Django уже написал за нас.
Важно сразу избавиться от заблуждения: CBV — это не более «правильный» или «продвинутый» способ писать представления. Это просто другой инструмент. Профессиональные Django-проекты используют оба подхода одновременно, выбирая каждый раз по ситуации.
Самый базовый класс — django.views.generic.View. Все остальные CBV в Django наследуются от него, прямо или через промежуточные классы.
# films/views.py
from django.views.generic import View
from django.http import HttpResponse
class HelloView(View):
def get(self, request):
return HttpResponse('Привет из классового представления!')Ключевая идея View: вместо одной функции, которая проверяет request.method через if, — отдельный метод для каждого HTTP-метода. get() обрабатывает GET-запросы, post() — POST-запросы, и так далее.
class AddFilmView(View):
def get(self, request):
form = FilmForm()
return render(request, 'films/add_film.html', {'form': form})
def post(self, request):
form = FilmForm(request.POST, request.FILES)
if form.is_valid():
film = form.save()
return redirect(film.get_absolute_url())
return render(request, 'films/add_film.html', {'form': form})Сравним с FBV-версией того же представления из урока 18:
# FBV-версия — для сравнения
def add_film(request):
if request.method == 'POST':
form = FilmForm(request.POST, request.FILES)
if form.is_valid():
film = form.save()
return redirect(film.get_absolute_url())
else:
form = FilmForm()
return render(request, 'films/add_film.html', {'form': form})Разница пока невелика — это ожидаемо для простого случая. Преимущество CBV раскроется, когда мы перейдём к готовым базовым классам — ListView, DetailView, CreateView в следующих уроках, где Django уже реализовал типовую логику внутри.
CBV нельзя передать в path() как обычный класс — нужно вызвать специальный метод as_view(), который превращает класс в функцию, понятную Django:
# films/urls.py
from .views import AddFilmView
urlpatterns = [
# ...
path('films/add/', AddFilmView.as_view(), name='add_film'),
]as_view() — это фабричный метод, который на каждый запрос создаёт новый экземпляр класса и вызывает у него подходящий метод (get, post и так далее) в зависимости от метода HTTP-запроса.
TemplateView — это специализированное представление, построенное поверх View. Оно предназначено для отображения HTML-шаблонов и уже содержит готовую реализацию обработки GET-запроса. Готовый класс для страниц, которым нужно просто отрендерить шаблон без сложной логики. Он отлично подходит для статических страниц вроде «О сайте», «Контакты», «Политика конфиденциальности» и подобных.
Самый простой вариант использования выглядит так:
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = 'films/about.html'
extra_context = {
'title': 'О нашем сайте',
}Здесь используются два атрибута класса:
template_name— указывает, какой шаблон нужно отобразить.extra_context— позволяет передать в шаблон дополнительные постоянные данные.
Такой подход хорошо подходит для статического контекста, который не зависит от запроса или базы данных.
На практике часто возникает необходимость передать в шаблон данные, которые вычисляются во время выполнения запроса.
Например:
- количество фильмов в базе;
- список жанров;
- текущего пользователя;
- результаты запросов к базе данных.
В таких случаях используют метод get_context_data().
class AboutView(TemplateView):
template_name = 'films/about.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'О нашем сайте'
context['film_count'] = Film.objects.count()
return contextИменно этот способ чаще всего используется в реальных проектах, потому что он позволяет сформировать контекст любой сложности.
Разберём этот паттерн подробнее, поскольку он будет постоянно встречаться во всех следующих CBV.
get_context_data() — это метод, который формирует словарь контекста для шаблона. По умолчанию Django уже создает базовый контекст. Наша задача — получить его, дополнить своими данными и вернуть обратно.
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # 1. Получаем базовый контекст
context['title'] = 'О нашем сайте' # 2. Добавляем свои данные
context['film_count'] = Film.objects.count()
return context # 3. Возвращаем итоговый словарьЗдесь происходит три последовательных шага:
- Получаем базовый контекст родительского класса.
- Добавляем собственные данные.
- Возвращаем итоговый словарь.
Очень важно не забывать вызывать super().get_context_data(**kwargs). Если этого не сделать, можно потерять данные, которые Django автоматически добавляет в контекст (например, параметры, полученные из URL).
# films/urls.py
from .views import AboutView
urlpatterns = [
path('about/', AboutView.as_view(), name='about'),
]Одна из ключевых идей CBV — заменять небольшие логические операции декларативными атрибутами класса. Это уже видно в TemplateView.template_name — вместо вызова render(request, 'films/about.html') мы просто объявляем имя шаблона как атрибут, а сам вызов render() происходит внутри родительского класса.
Эта идея будет развиваться в следующих уроках: ListView использует атрибут model вместо ручного вызова Model.objects.all(), DetailView — атрибуты model и slug_field вместо get_object_or_404(). Сейчас достаточно увидеть сам принцип на примере template_name.
Перепишем index (главная страница) через TemplateView — это представление идеально подходит для демонстрации, потому что оно простое и статичное:
# FBV-версия (была с урока 1)
def index(request):
context = {'title': 'Лучшие фильмы всех времён'}
return render(request, 'films/index.html', context)# CBV-версия
class IndexView(TemplateView):
template_name = 'films/index.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Лучшие фильмы всех времён'
return context# films/urls.py
from .views import IndexView
urlpatterns = [
path('', IndexView.as_view(), name='index'),
]Обе версии абсолютно равноценны по результату. Какую выбрать — вопрос команды и личных предпочтений. Часто проект придерживается одного подхода для однотипных страниц, чтобы код был предсказуемым.
Это не строгое правило, а ориентир, который вырабатывается с опытом:
| Ситуация | Предпочтительный подход |
|---|---|
| Простая логика, один HTTP-метод | FBV — короче и нагляднее |
| Стандартный CRUD (список, деталь, создание, редактирование, удаление) | CBV с готовыми generic views — меньше повторяющегося кода |
| Сложная нестандартная логика, ветвления, несколько форм на одной странице | FBV — проще читать линейную логику |
| Нужно расширяемое поведение через наследование (например, общая логика для нескольких похожих страниц) | CBV — наследование классов для этого создано |
| API-эндпоинт с обработкой нескольких методов (GET, POST, PUT, DELETE) | CBV — структура View с отдельными методами естественно подходит |
Важная честная оговорка. В реальных проектах эта грань размыта, и многие опытные разработчики Django предпочитают FBV почти везде, аргументируя это явностью и простотой отладки — в функции весь поток исполнения виден сразу, без необходимости прыгать по цепочке родительских классов. Другие предпочитают CBV за компактность для типовых задач. Мы изучим оба подхода, чтобы ты мог читать код, написанный в любом стиле, и сознательно выбирать инструмент под задачу.
# TypeError — Django получит сам класс, а не функцию-обработчик
path('about/', AboutView, name='about'),
# Правильно — as_view() возвращает функцию
path('about/', AboutView.as_view(), name='about'),# Опасно — может потерять служебные данные, которые добавляет родительский класс
def get_context_data(self, **kwargs):
return {'title': 'О сайте'}
# Правильно — сначала берём базовый контекст, потом дополняем
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'О сайте'
return contextЕсли в View определить метод с произвольным именем (не get, post, put, delete и так далее) — Django его просто не вызовет, и запрос с соответствующим HTTP-методом получит 405 Method Not Allowed:
class AddFilmView(View):
def get(self, request):
...
def save_film(self, request): # Django не знает про этот метод
...Имя метода должно строго соответствовать HTTP-методу в нижнем регистре: get, post, put, patch, delete, head, options.
# films/views.py
from django.views.generic import View, TemplateView
from django.shortcuts import render, redirect
from django.http import HttpResponse
from .forms import FilmForm
from .models import Film
class IndexView(TemplateView):
template_name = 'films/index.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Лучшие фильмы всех времён'
return context
class AboutView(TemplateView):
template_name = 'films/about.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'О нашем сайте'
context['film_count'] = Film.objects.count()
return context
class AddFilmView(View):
def get(self, request):
form = FilmForm()
return render(request, 'films/add_film.html', {'form': form})
def post(self, request):
form = FilmForm(request.POST, request.FILES)
if form.is_valid():
film = form.save()
return redirect(film.get_absolute_url())
return render(request, 'films/add_film.html', {'form': form})# films/urls.py
from django.urls import path
from .views import IndexView, AboutView, AddFilmView
from . import views
app_name = 'films'
urlpatterns = [
path('', IndexView.as_view(), name='index'),
path('about/', AboutView.as_view(), name='about'),
path('films/add/', AddFilmView.as_view(), name='add_film'),
# остальные маршруты пока остаются как FBV — переведём в следующих уроках
path('films/', views.film_list, name='film_list'),
path('films/search/', views.search_film, name='search_film'),
path('films/<slug:slug>/', views.film_detail, name='film_detail'),
path('films/<slug:slug>/review/', views.add_review, name='add_review'),
path('directors/<slug:slug>/', views.director_detail, name='director_detail'),
path('stats/', views.catalog_stats, name='catalog_stats'),
]- Зачем нужен метод
as_view()при подключении CBV вurls.py? - В чём принципиальное отличие класса
Viewот FBV в плане обработки разных HTTP-методов? - Зачем переопределять
get_context_data(), а не просто передавать словарь напрямую, как вrender()? - Что произойдёт, если в классе
Viewопределить метод с именем, не соответствующим HTTP-методу — например,def handle_post(self, request)? - Почему не существует универсального правила «всегда используй CBV» или «всегда используй FBV»?
Тип: переведи
Переведи представление catalog_stats (страница со статистикой каталога, написанная в уроке 11) с FBV на CBV, используя TemplateView.
Требования:
- Создай класс
CatalogStatsView, наследующийTemplateView - Укажи
template_name = 'films/catalog_stats.html' - Перенеси всю логику из тела функции
catalog_statsвнутрьget_context_data() - Обнови маршрут в
films/urls.py, заменивviews.catalog_statsнаCatalogStatsView.as_view()
Исходная FBV-версия для справки:
def catalog_stats(request):
overall_stats = Film.objects.aggregate(
total=Count('id'),
avg_rating=Avg('rating'),
max_rating=Max('rating'),
min_rating=Min('rating'),
)
top_directors = Director.objects.annotate(
film_count=Count('films')
).filter(film_count__gt=0).order_by('-film_count')[:5]
films_by_year = Film.objects.values('year').annotate(count=Count('id')).order_by('-year')
context = {
'overall_stats': overall_stats,
'top_directors': top_directors,
'films_by_year': films_by_year,
}
return render(request, 'films/catalog_stats.html', context)