LibrariesWhy do Django views need an as_view() method?

Why do Django views need an as_view() method?

One thing I’ve sometimes wondered about when using Django: Why do view objects need to be constructed in a special way?

For those of you who haven’t used Django, a typical URL mapping scheme looks like this:

urlpatterns = [
    url(r'^login/$', views.LoginView.as_view(), name='login'),
    url(r'^logout/$', views.LogoutView.as_view(), name='logout'),
    # ...
]

This maps URLs to view classes in a fairly obvious way. A view object is just an instance that handles an HTTP request and returns an appropriate response (actually combining both Model and View, for those of you used to thinking in a more orthodox MVC structure).

The LoginView.as_view() call is actually calling a static method on the LoginView class, which constructs and returns an instance of LoginView. This is basically the Factory Method pattern.

But why can’t this just be:

    url(r'^login/$', views.LoginView(), name='login')

This isn’t just a cosmetic detail either. Sometimes you want to pass arguments in to customise the view instance, and if you want to do this you have to pass keyword arguments to as_view(). These aren’t actually passed as arguments to __init__() either; keyword arguments to as_view() can only assign to attribues already declared on the class.

So if we want to customise our LoginView, we have to do something like:

class LoginView(View):
    require_staff = False
 
    # ...
 
# later...
 
urlpatterns = [
    url(r'^login/$', views.LoginView(require_staff=True), name='login'),
    # ...
]

First, let’s look at what Django is actually doing, then ask why. Here’s the as_view() method in the base class (minus a little bit of parameter checking code):

def as_view(cls, **initkwargs):
    """
    Main entry point for a request-response process.
    """
    for key in initkwargs:
        # ... some checking on args
 
    def view(request, *args, **kwargs):
        self = cls(**initkwargs)
        if hasattr(self, 'get') and not hasattr(self, 'head'):
            self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return self.dispatch(request, *args, **kwargs)
    view.view_class = cls
    view.view_initkwargs = initkwargs
 
    # take name and docstring from class
    update_wrapper(view, cls, updated=())
 
    # and possible attributes set by decorators
    # like csrf_exempt from dispatch
    update_wrapper(view, cls.dispatch, assigned=())
    return view

This constructs a local function, sets some properties on it and returns the function to the caller. The function will be called later, when the HTTP request is received. When this function is called, the LoginView is finally constructed and the dispatch() method is called (which is what deals with the HTTP request and returns a response).

So the short answer is that as_view() is needed because the Django code is expecting a function, not an object. This is partly for historical reasons: Django used to exclusively use functions as views, and introduced objects more recently. Now it’s possible to use either objects or functions as views.

The obvious response is to ask why the Django dispatch code cares whether it has a function or an object (or, equivalently, to claim that a function is an object). Python is supposed to use duck typing, so code shouldn’t care what sort of object it has provided it behaves correctly. In this context, “behaves correctly” means that the object is callable. And we’re perfectly capable of creating our own objects that are callable in just the same way functions are:

class LoginView(object):
    def __call__(self, request, *args, **kwargs):
        return self.dispatch(request, args, **kwargs)

If we construct one of these and give it to Django, it should be completely unaware that this is not a normal Python function. In fact, this is true. The Django code that executes the view function looks like this:

resolver_match = resolver.resolve(request.path_info)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match
 
# Apply view middleware
# ...
 
if response is None:
    wrapped_callback = self.make_view_atomic(callback)
    try:
        response = wrapped_callback(request, *callback_args, **callback_kwargs)
    except Exception as e:
        response = self.process_exception_by_middleware(e, request)

Here callback is just the view function (either a function or the result of calling .as_view() on a view class) that we assigned in the URL pattern. We can ignore what wrapped_callback does for our current purposes, and just assume that it’s some kind of wrapper function around the original callback. So we are indeed just calling the function, and it won’t detect the difference between a real function and a class with a __call__ method.

You can prove this yourself by creating a custom view class that has a __call__ method:

class SpecialView:
    def __call__(self, request, *args, **kwargs):
        return HttpResponse()
 
urlpatterns = [
    url(r'^foo/', SpecialView())
]

Django will happily call your SpecialView instance as if it were a real function.

But notice a key difference between this and the wrapper function generated by Django: If you use SpecialView you have a single object instance for as long as the Django service is running. The Django wrapper function constructs a new instance for every request.

So this is the real reason why .as_view() is desirable: it means that no state is held by the View class between one request and the next. If Django didn’t do this, you’d need to be excessively careful that you didn’t assign to some member of the view that would lead to different behaviour next time the view was run, and you’d end up with a nightmare to debug.

Categories: Libraries Tags:

Comments

No Comments Yet. Be the first?

Post a comment

Your email address will not be published. Required fields are marked *