Команда 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. Я избегаю и представлений на функциях, и сложных обобщённых классовых представлений. Для меня это идеальная золотая середина. Такой подход даёт чистую организацию кода по методам запроса (getpostput и т.д.) и автоматически обрабатывает ответ 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)


  1. egorro_13
    15.09.2025 08:12

    Вы могли заметить небольшое дублирование: get_object_or_404 вызывается и в get, и в post. «Книжный» способ решить это при использовании базового класса View — переопределить метод dispatch. Он выполняется до вызова get или post, поэтому логично поместить туда подготовительную логику

    Для этого есть отдельный метод setup


    1. danilovmy
      15.09.2025 08:12

      Согласен, setup классный. Только он вызывается до вызова dispatch, и если у Kevin Renskers стоит какой-нибудь dispatch декоратор через method_decorator то может произойти вызов объекта раньше чем это вообще надо... хотя о чем это я, Kevin не такой... ну я про то, что он не стал бы использовать декораторы...


  1. danilovmy
    15.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. Однако, я пробежал по комиттам и не нашел ни одной написанной им строчки кода, мне это показалось очень странным. Хочу верить, что я просто ошибаюсь.