Введение в представления на основе классов

Представления на основе классов обеспечивают альтернативный способ реализации представлений в виде объектов Python вместо функций. Они не заменяют представления на основе функций, но имеют определенные отличия и преимущества по сравнению с представлениями на основе функций:

  • Организация кода, связанного с конкретными методами HTTP (GET, POST и т.д.), может быть решена отдельными методами вместо условного ветвления.
  • Для разделения кода на многократно используемые компоненты можно использовать объектно-ориентированные методы, такие как миксины (множественное наследование).

Взаимосвязь и история общих представлений, представлений на основе классов и общих представлений на основе классов

В начале был только контракт функции представления, Django передавал вашей функции HttpRequest и ожидал обратно HttpResponse. Это был предел того, что предоставлял Django.

В самом начале было признано, что существуют общие идиомы и шаблоны, встречающиеся при разработке представлений. Для абстрагирования этих паттернов и облегчения разработки представлений для общих случаев были введены общие представления на основе функций.

Проблема с общими представлениями на основе функций заключается в том, что, хотя они хорошо покрывали простые случаи, не было возможности расширить или настроить их помимо некоторых простых опций конфигурации, что ограничивало их полезность во многих реальных приложениях.

Обобщенные представления на основе классов были созданы с той же целью, что и обобщенные представления на основе функций, - упростить разработку представлений. Однако способ реализации решения - использование миксинов - обеспечивает инструментарий, который приводит к тому, что общие представления на основе классов являются более расширяемыми и гибкими, чем их аналоги на основе функций.

Если в прошлом вы пробовали использовать общие представления, основанные на функциях, и нашли их недостаточными, вам не следует думать об общих представлениях, основанных на классах, как о простом эквиваленте, основанном на классах, а скорее как о новом подходе к решению исходных проблем, которые должны были решать общие представления.

Набор базовых классов и миксинов, которые Django использует для создания общих представлений на основе классов, создан для максимальной гибкости, и поэтому имеет множество крючков в виде реализации методов по умолчанию и атрибутов, с которыми вы вряд ли будете иметь дело в самых простых случаях использования. Например, вместо того, чтобы ограничивать вас атрибутом класса для form_class, реализация использует метод get_form, который вызывает метод get_form_class, который в реализации по умолчанию просто возвращает атрибут класса form_class. Это дает вам несколько вариантов указания того, какую форму использовать, от простого атрибута до полностью динамического, вызываемого хука. Эти опции кажутся полыми сложностями для простых ситуаций, но без них более продвинутые конструкции были бы ограничены.

Использование представлений на основе классов

По своей сути представление на основе классов позволяет вам отвечать на различные методы HTTP-запросов с помощью различных методов экземпляра класса, а не с помощью условно ветвящегося кода внутри одной функции представления.

Таким образом, код для обработки HTTP GET в функции представления будет выглядеть примерно так:

from django.http import HttpResponse

def my_view(request):
    if request.method == 'GET':
        # <view logic>
        return HttpResponse('result')

В представлении, основанном на классах, это будет выглядеть так:

from django.http import HttpResponse
from django.views import View

class MyView(View):
    def get(self, request):
        # <view logic>
        return HttpResponse('result')

Поскольку преобразователь URL в Django ожидает отправки запроса и связанных с ним аргументов в вызываемую функцию, а не в класс, представления на основе классов имеют метод класса as_view(), который возвращает функцию, которая может быть вызвана, когда приходит запрос на URL, соответствующий связанному шаблону. Функция создает экземпляр класса, вызывает setup() для инициализации его атрибутов, а затем вызывает его метод dispatch(). dispatch рассматривает запрос, чтобы определить, является ли он GET, POST и т.д., и передает запрос соответствующему методу, если он определен, или вызывает HttpResponseNotAllowed, если нет:

# urls.py
from django.urls import path
from myapp.views import MyView

urlpatterns = [
    path('about/', MyView.as_view()),
]

Стоит отметить, что то, что возвращает ваш метод, идентично тому, что вы возвращаете из представления на основе функции, а именно некоторую форму HttpResponse. Это означает, что объекты http shortcuts или TemplateResponse допустимо использовать внутри представления, основанного на классе.

Хотя минимальное представление на основе классов не требует никаких атрибутов классов для выполнения своей работы, атрибуты классов полезны во многих конструкциях на основе классов, и есть два способа конфигурирования или установки атрибутов классов.

Первый - это стандартный для Python способ подклассирования и переопределения атрибутов и методов в подклассе. Таким образом, если ваш родительский класс имеет атрибут greeting следующим образом:

from django.http import HttpResponse
from django.views import View

class GreetingView(View):
    greeting = "Good Day"

    def get(self, request):
        return HttpResponse(self.greeting)

Вы можете переопределить это в подклассе:

class MorningGreetingView(GreetingView):
    greeting = "Morning to ya"

Другой вариант - настроить атрибуты класса в качестве аргументов ключевых слов для вызова as_view() в URLconf:

urlpatterns = [
    path('about/', GreetingView.as_view(greeting="G'day")),
]

Примечание

Хотя ваш класс инстанцируется для каждого запроса, направленного к нему, атрибуты класса, установленные через точку входа as_view(), настраиваются только один раз во время импорта ваших URL.

Использование миксинов

Миксины - это форма множественного наследования, когда поведение и атрибуты нескольких родительских классов могут быть объединены.

Например, в общих представлениях, основанных на классах, есть миксин TemplateResponseMixin, основной целью которого является определение метода render_to_response(). В сочетании с поведением базового класса View в результате получается класс TemplateView, который будет отправлять запросы соответствующим методам (поведение, определенное в базовом классе View), и который имеет метод render_to_response(), использующий атрибут template_name для возврата объекта TemplateResponse (поведение, определенное в TemplateResponseMixin).

Миксины - это отличный способ повторного использования кода в нескольких классах, но за это приходится платить. Чем больше ваш код разбросан по миксинам, тем сложнее будет прочитать дочерний класс и понять, что именно он делает, и тем сложнее будет понять, какие методы из каких миксинов нужно переопределить, если вы подклассифицируете что-то с глубоким деревом наследования.

Обратите также внимание, что вы можете наследоваться только от одного общего вида - то есть, только один родительский класс может наследоваться от View, а остальные (если они есть) должны быть миксинами. Попытка наследоваться от более чем одного класса, который наследуется от View - например, попытка использовать форму в верхней части списка и объединение ProcessFormView и ListView - не будет работать так, как ожидается.

Работа с формами с помощью представлений на основе классов

Базовое представление на основе функций, которое работает с формами, может выглядеть примерно так:

from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import MyForm

def myview(request):
    if request.method == "POST":
        form = MyForm(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect('/success/')
    else:
        form = MyForm(initial={'key': 'value'})

    return render(request, 'form_template.html', {'form': form})

Подобное представление на основе классов может выглядеть следующим образом:

from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views import View

from .forms import MyForm

class MyFormView(View):
    form_class = MyForm
    initial = {'key': 'value'}
    template_name = 'form_template.html'

    def get(self, request, *args, **kwargs):
        form = self.form_class(initial=self.initial)
        return render(request, self.template_name, {'form': form})

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        if form.is_valid():
            # <process form cleaned data>
            return HttpResponseRedirect('/success/')

        return render(request, self.template_name, {'form': form})

Это очень простой случай, но вы можете видеть, что у вас будет возможность настроить это представление, переопределив любой из атрибутов класса, например form_class, через конфигурацию URLconf, или подкласс и переопределив один или несколько методов (или оба!).

Украшение представлений на основе классов

Расширение представлений на основе классов не ограничивается использованием миксинов. Вы также можете использовать декораторы. Поскольку представления на основе классов не являются функциями, их декорирование работает по-разному в зависимости от того, используете ли вы as_view() или создаете подкласс.

Декорирование в URLconf

Самый простой способ декорирования представлений на основе классов - это декорирование результата метода as_view(). Проще всего это сделать в URLconf, где вы развертываете свое представление:

from django.contrib.auth.decorators import login_required, permission_required
from django.views.generic import TemplateView

from .views import VoteView

urlpatterns = [
    path('about/', login_required(TemplateView.as_view(template_name="secret.html"))),
    path('vote/', permission_required('polls.can_vote')(VoteView.as_view())),
]

При таком подходе декоратор применяется к каждому экземпляру. Если вы хотите, чтобы каждый экземпляр представления был украшен, вам нужно использовать другой подход.

Украшение класса

Чтобы украсить каждый экземпляр представления, основанного на классе, необходимо украсить само определение класса. Для этого нужно применить декоратор к методу dispatch() класса.

Метод в классе - это не совсем то же самое, что отдельная функция, поэтому вы не можете просто применить декоратор функции к методу - вам нужно сначала преобразовать его в декоратор метода. Декоратор method_decorator преобразует декоратор функции в декоратор метода, чтобы его можно было использовать для метода экземпляра. Например:

from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView

class ProtectedView(TemplateView):
    template_name = 'secret.html'

    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

Или, более кратко, вы можете украсить класс и передать имя украшаемого метода в качестве аргумента ключевого слова name:

@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
    template_name = 'secret.html'

Если у вас есть набор общих декораторов, используемых в нескольких местах, вы можете определить список или кортеж декораторов и использовать его вместо того, чтобы вызывать method_decorator() несколько раз. Эти два класса эквивалентны:

decorators = [never_cache, login_required]

@method_decorator(decorators, name='dispatch')
class ProtectedView(TemplateView):
    template_name = 'secret.html'

@method_decorator(never_cache, name='dispatch')
@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
    template_name = 'secret.html'

Декораторы будут обрабатывать запрос в том порядке, в котором они переданы декоратору. В примере never_cache() обработает запрос раньше, чем login_required().

В этом примере каждый экземпляр ProtectedView будет иметь защиту от входа. В этих примерах используется login_required, однако, такое же поведение можно получить более простым способом, используя LoginRequiredMixin.

Примечание

method_decorator передает *args и **kwargs в качестве параметров декорированному методу на классе. Если ваш метод не принимает совместимый набор параметров, он вызовет исключение TypeError.

Вернуться на верх