Команда 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 и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!
Комментарии (0)
 - danilovmy15.09.2025 08:12- @python_leader спасибо за перевод. - Перевожу мой комментарий к оригиналу статьи: - Забавная статья. Автор не хочет запоминать другие «магические» методы и по-прежнему использует магию, такую как get_object_or_404, redirect или что-то еще. 
 А если автор хочет избежать дублирования кода, он предложил поместить код в «dispatch». Как он предлагает изменить dispatch? Используя тот же код, что и в методе get_object из SingleObjectMixin.- Субъективно, но однострочный миксин в определении класса все же проще, чем import + переопределение метода + ... - Но в общем подход автора показывает, что он, вероятно, не понимает важную идею GCBV: декларативный подход. - Декларативный подход означает меньше кода. - Декларативный подход означает меньше тестирования. - Декларативный подход означает меньшую сложность (например, можно использовать метрики Халстеда). - Декларативный подход означает меньшую цикломатическую сложность. - Декларативный подход означает меньше документации. - Я согласен с этой статьей, если автор зарабатывает деньги в зависимости от количества строк кода. В этом случае Black-formatter также может помочь получить гораздо больше строк. - P.s. Kevin Renskers ссылается на туториал от Luke Plant, контрибьютора в те самые DGBV. Люк сожалеет (I hate it when that happens…), что попал в список соавторов django.views.generic. Однако, я пробежал по комиттам и не нашел ни одной написанной им строчки кода, мне это показалось очень странным. Хочу верить, что я просто ошибаюсь.  - andreymal15.09.2025 08:12- Лично для меня любая декларативность наоборот повышает сложность, так как приходится тратить дополнительные усилия на то, чтобы понять, как же оно там реально работает внутри. А с императивностью всё просто сразу на виду без лишних абстракций - однострочный миксин в определении класса все же проще - Нет, явный вызов конкретной функции всегда проще и понятнее, чем прятать всё за миксином, который переопределяет непонятно что непонятно в каком порядке (я не желаю гулять по всей иерархии классов, чтобы выяснить, да что же блин происходит в моём коде, и поэтому не использую CBV в своих проектах, кстати удачи с ромбовидным наследованием) - Но код, написанный автором, мне тоже не нравится: автор зачем-то использовал класс, продолбал аннотации типов, разделил фактически идентичные get и post и из-за этого оказался вынужден обмазаться функциями-хелперами, от которых можно было бы спокойно избавиться: - def comment_form(request: HttpRequest, post_id: int) -> HttpResponse: post = get_object_or_404(Post, pk=post_id) if request.method == "POST": form = CommentForm(request.POST) if form.is_valid(): comment = form.save(commit=False) comment.post = post comment.save() return redirect(post) else: form = CommentForm() return render(request, "form.html", {"post": post, "form": form})- Это паттерн, который я использую в своих проектах и который даже совпадает с тем, что написано в документации Django — код не только в два раза короче, но и, по моему субъективному мнению, НАМНОГО проще для понимания и дальнейшей поддержки. Не надо скакать по иерархии классов, не надо даже скакать по функциям — всё на виду и максимально очевидно 
 
 
           
 
egorro_13
Для этого есть отдельный метод setup
danilovmy
Согласен, setup классный. Только он вызывается до вызова dispatch, и если у Kevin Renskers стоит какой-нибудь dispatch декоратор через method_decorator то может произойти вызов объекта раньше чем это вообще надо... хотя о чем это я, Kevin не такой... ну я про то, что он не стал бы использовать декораторы...