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).
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)
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
You can prove this yourself by creating a custom view class that has a
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.