Команда Python for Devs подготовила перевод статьи о том, как автор выбирает способ написания представлений в Django. Он считает, что обобщённые классовые представления (CBV) скрывают слишком много магии, усложняют чтение кода и отладку. Вместо них он использует базовый View, чтобы сохранять контроль, но при этом избегать громоздких if в функциях.
Когда изучаете Django, одна из первых серьезных развилок — как писать представления. Django предлагает два основных подхода: простые функции или мощные классы. Официальный туториал сначала аккуратно знакомит с представлениями на функциях.
def index(request):
return HttpResponse("Hello, world. You're at the polls index.")
Затем становится немного сложнее, но все еще используются представления на функциях:
def index(request):
latest_question_list = Question.objects.order_by("-pub_date")[:5]
context = {"latest_question_list": latest_question_list}
return render(request, "polls/index.html", context)
Но совсем скоро он переходит к обобщенным классовым представлениям (CBV):
class IndexView(generic.ListView):
template_name = "polls/index.html"
context_object_name = "latest_question_list"
def get_queryset(self):
"""Вернуть последние пять опубликованных вопросов."""
return Question.objects.order_by("-pub_date")[:5]
Я считаю, что это ошибка. В Django очень много обобщенных представлений: View
, TemplateView
, DetailView
, ListView
, FormView
, CreateView
, DeleteView
, UpdateView
, RedirectView
, а еще целая россыпь представлений, завязанных на даты: ArchiveIndexView
, YearArchiveView
, MonthArchiveView
, WeekArchiveView
, DayArchiveView
, TodayArchiveView
, DateDetailView
.
Самая большая проблема, которую я вижу в этих представлениях, — их скрытая сложность. Достаточно взглянуть на документацию DetailView
. Чтобы понять работу одного этого класса, нужно разобраться в его дереве наследования:
django.views.generic.detail.SingleObjectTemplateResponseMixin
django.views.generic.base.TemplateResponseMixin
django.views.generic.detail.BaseDetailView
django.views.generic.detail.SingleObjectMixin
django.views.generic.base.View
А затем нужно знать порядок разрешения методов (MRO) и то, какие вызовы происходят внутри. «Диаграмма методов» включает:
setup()
dispatch()
http_method_not_allowed()
get_template_names()
get_slug_field()
get_queryset()
get_object()
get_context_object_name()
get_context_data()
get()
render_to_response()
Это 11 методов, разбросанных по 5 классам и примесям (mixins). Отладка такого представления или попытка понять, какой именно метод нужно переопределить, чтобы изменить его поведение, быстро превращается в бесконечное открывание файлов и прыжки между определениями методов. Это слишком стрёмно.
Предполагаемое преимущество заключается в упрощении кода и уменьшении его объема, но на практике для простых представлений это почти не экономит строчек, а для сложных вы чаще всего боретесь с поведением, которое задано по умолчанию в обобщённых представлениях.
Документации по всем этим классам и примесям (mixins) так много, что проще не становится. Именно поэтому мне так близка позиция, которую озвучивает Люк Плант в статье Django Views — The Right Way. Он призывает использовать представления на функциях во всех случаях. Вот как он объясняет свою точку зрения:
Одна из причин, по которой я рекомендую такой подход, — он даёт отличную отправную точку для любых задач. Тело представления — функция, которая принимает запрос и возвращает ответ — находится прямо перед глазами… Если разработчик понимает, что такое представление, он, скорее всего, сразу догадается, какой код нужно написать. Структура кода не станет препятствием. С CBV всё иначе: как только появляется какая-то логика, нужно знать, какие методы или атрибуты определять, а это значит разбираться в огромном API.
Это отличный гайд, который показывает, как привычные паттерны CBV можно реализовать более явно и зачастую лаконичнее с помощью функций. Очень рекомендую его прочитать.
Однако в своих проектах я иду немного другим путем: использую только базовый класс View
. Я избегаю и представлений на функциях, и сложных обобщённых классовых представлений. Для меня это идеальная золотая середина. Такой подход даёт чистую организацию кода по методам запроса (get
, post
, put
и т.д.) и автоматически обрабатывает ответ 405 Method Not Allowed
.
То есть вместо представления на функции с большим блоком if
:
def comment_form_view(request, post_id):
post = get_object_or_404(Post, pk=post_id)
if request.method == "POST":
form = CommentForm(data=request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.save()
return redirect(post) # assumes Post has get_absolute_url()
else:
form = CommentForm()
return TemplateResponse(request, "form.html", {"form": form, "post": post})
я пишу так:
class CommentFormView(View):
def get(self, request, post_id, *args, **kwargs):
post = get_object_or_404(Post, pk=post_id)
form = CommentForm()
return TemplateResponse(request, "form.html", {"form": form, "post": post})
def post(self, request, post_id, *args, **kwargs):
post = get_object_or_404(Post, pk=post_id)
form = CommentForm(data=request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.save()
return redirect(post)
return TemplateResponse(request, "form.html", {"form": form, "post": post})
Хотя версия на классе получается на несколько строк длиннее, разделение логики GET
и POST
выглядит куда чище, чем вложение основной обработки POST
внутрь if request.method == "POST"
.
Вы могли заметить небольшое дублирование: get_object_or_404
вызывается и в get
, и в post
. «Книжный» способ решить это при использовании базового класса View
— переопределить метод dispatch
. Он выполняется до вызова get
или post
, поэтому логично поместить туда подготовительную логику:
class CommentFormView(View):
def dispatch(self, request, post_id, *args, **kwargs):
self.post_obj = get_object_or_404(Post, pk=post_id)
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
form = CommentForm()
return TemplateResponse(request, "form.html", {"form": form, "post": self.post_obj})
def post(self, request, *args, **kwargs):
form = CommentForm(data=request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.post = self.post_obj
comment.save()
return redirect(self.post_obj)
return TemplateResponse(request, "form.html", {"form": form, "post": self.post_obj})
Однако в собственном коде я почти не использую этот приём — он кажется мне немного «магическим». Вместо явного вызова метода мы полагаемся на ещё одну особенность реализации View
, о которой нужно помнить.
В простых случаях вроде этого небольшое дублирование, как ни странно, чаще всего оказывается самым понятным вариантом. Всё явно, и не требуется никакого умственного усилия, чтобы понять, что происходит в get
и post
. Если подготовительная логика становится сложнее или появляется общий контекст с большим числом переменных, то вместо dispatch
я выношу её в простой вспомогательный метод и вызываю его из обоих мест. Так сохраняется явный контроль потока выполнения.
class CommentFormView(View):
def get_shared_context(self, request, post_id):
# Представим, что здесь возвращается не только одна переменная post 😅
post = get_object_or_404(Post, pk=post_id)
return {"post": post}
def get(self, request, post_id, *args, **kwargs):
form = CommentForm()
context = self.get_shared_context(request, post_id) | {"form": form}
return TemplateResponse(request, "form.html", context)
def post(self, request, post_id, *args, **kwargs):
form = CommentForm(data=request.POST)
context = self.get_shared_context(request, post_id) | {"form": form}
post = context["post"]
if form.is_valid():
comment = form.save(commit=False)
comment.post = post
comment.save()
return redirect(post)
return TemplateResponse(request, "form.html", context)
Для меня это идеальный вариант. Мы устранили дублирование кода, но сделали это максимально явно. Методы get
и post
полностью контролируют процесс, нет никакого «магического» состояния, которое где-то устанавливается за кулисами. Получается простота и прозрачность функций, но с лучшей организацией кода, автоматической обработкой HTTP-методов и возможностью разделять общую логику так, как удобно нам.
И да, в самой базовой форме FormView
в Django короче:
class CommentFormView(FormView):
template_name = "form.html"
form_class = CommentForm
def form_valid(self, form):
post = get_object_or_404(Post, pk=self.kwargs["post_id"])
comment = form.save(commit=False)
comment.post = post
comment.save()
return redirect(post)
Но стоит только захотеть добавить свою логику в обработку GET
(например, расширить контекст), по-разному обрабатывать результаты POST
или настроить обработку ошибок — и вы быстро приходите к переопределению множества методов. В этот момент снова приходится разбирать внутренности фреймворка, и первоначальная краткость оборачивается сложностью. Мой подход держит всю логику прямо перед глазами — каждый раз.
Русскоязычное сообщество про Python

Друзья! Эту статью перевела команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!