django --- DetailView原始碼分析
【背景】
最近在看django官方文件的class-based-views這一節的時候一直不得要領,感覺自己清楚,但是回想起來又沒有脈絡;於是沒有辦法只
能是“暗中觀察”django的原始碼了。 剛開啟原始碼看了沒有多久就疑竇叢生,比如說下面這一段,能看的出get_object方法中用到的self.kwargs
屬性是在哪裡設定過呢?如果沒有設定直接用是會有報錯的,詳細內容看下面原始碼
class SingleObjectMixin(ContextMixin): """ Provide the ability to retrieve a single object for further manipulation.""" model = None queryset = None slug_field = 'slug' context_object_name = None slug_url_kwarg = 'slug' pk_url_kwarg = 'pk' query_pk_and_slug = False def get_object(self, queryset=None): """ Return the object the view is displaying. Require `self.queryset` and a `pk` or `slug` argument in the URLconf. Subclasses can override this to return any object.""" # Use a custom queryset if provided; this is required for subclasses # like DateDetailView if queryset is None: queryset = self.get_queryset() # Next, try looking up by primary key. pk = self.kwargs.get(self.pk_url_kwarg) slug = self.kwargs.get(self.slug_url_kwarg)if pk is not None: queryset = queryset.filter(pk=pk) # Next, try looking up by slug. if slug is not None and (pk is None or self.query_pk_and_slug): slug_field = self.get_slug_field() queryset = queryset.filter(**{slug_field: slug}) # If none of those are defined, it's an error. if pk is None and slug is None: raise AttributeError( "Generic detail view %s must be called with either an object " "pk or a slug in the URLconf." % self.__class__.__name__ ) try: # Get the single item from the filtered queryset obj = queryset.get() except queryset.model.DoesNotExist: raise Http404(_("No %(verbose_name)s found matching the query") % {'verbose_name': queryset.model._meta.verbose_name}) return obj def get_queryset(self): """ Return the `QuerySet` that will be used to look up the object. This method is called by the default implementation of get_object() and may not be called if get_object() is overridden. """ if self.queryset is None: if self.model: return self.model._default_manager.all() else: raise ImproperlyConfigured( "%(cls)s is missing a QuerySet. Define " "%(cls)s.model, %(cls)s.queryset, or override " "%(cls)s.get_queryset()." % { 'cls': self.__class__.__name__ } ) return self.queryset.all() def get_slug_field(self): """Get the name of a slug field to be used to look up by slug.""" return self.slug_field def get_context_object_name(self, obj): """Get the name to use for the object.""" if self.context_object_name: return self.context_object_name elif isinstance(obj, models.Model): return obj._meta.model_name else: return None def get_context_data(self, **kwargs): """Insert the single object into the context dict.""" context = {} if self.object: context['object'] = self.object context_object_name = self.get_context_object_name(self.object) if context_object_name: context[context_object_name] = self.object context.update(kwargs) return super().get_context_data(**context)
【眾裡尋它】
一看SingleObjectMixin類的定義發現它裡面並沒有kwargs這個屬性、所以這個kwargs屬性應該在是定義在它的父類裡、ContextMixin原始碼如下
class ContextMixin: """ A default context mixin that passes the keyword arguments received by get_context_data() as the template context. """ extra_context = None def get_context_data(self, **kwargs): kwargs.setdefault('view', self) if self.extra_context is not None: kwargs.update(self.extra_context) return kwargs
!我的天kwargs沒有在父類裡、也不可能在object類裡面。粗看起來get_object方法用使用self.kwargs已經違反Python這門語言的基本法了,但是
這個的可能性也不太;django這麼有名的一個專案,而且自己在使用這上塊的功能時並沒有出問題。
【刻苦學習在View.as_view方法中發現新的天地】
class View: http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) @classonlymethod def as_view(cls, **initkwargs): """Main entry point for a request-response process.""" #不允許給View的例項傳入get/post/head這樣的欄位、原因是這些名字是留給方法用的! for key in initkwargs: if key in cls.http_method_names: raise TypeError("You tried to pass in the %s method name as a " "keyword argument to %s(). Don't do that." % (key, cls.__name__)) #如下View類中已經定義了這個“名字”,為了防止例項覆蓋類的同名屬性所以這裡也不支援傳入同名屬性 if not hasattr(cls, key): raise TypeError("%s() received an invalid keyword %r. as_view " "only accepts arguments that are already " "attributes of the class." % (cls.__name__, key)) #如果上面的檢查都過了,但是django並沒有步入通常的套路。它在這裡定義了一個叫view的函式,它在view中建立View類的例項 def view(request, *args, **kwargs): #通過as_veiw中傳入的引數來建立一個view例項、由__init__可以知道它是對**kwargs全盤接收的 #initkwargs是通過閉包的形式記錄下來的 self = cls(**initkwargs) #在沒有指定`head`方法的情況下就直接用`get`來處理`head` if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get #下面這三行搞的對於任何一個通過上述方試創建出來的View例項都有了request、args、kwargs這三個屬性 self.request = request self.args = args self.kwargs = kwargs #呼叫 排程方法、以排程方法的返回結果為view這個Function的執行結果、由於排程方法會呼叫get | post | head ... 而這些方法都返回 #HttpResponse所以view最終返回的就是HttpResponse的例項 return self.dispatch(request, *args, **kwargs) view.view_class = cls view.view_initkwargs = initkwargs update_wrapper(view, cls, updated=()) update_wrapper(view, cls.dispatch, assigned=()) return view def dispatch(self, request, *args, **kwargs): """根據http的不同請求方式、呼叫不同的方法、並返回HttpResponse""" if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs) def http_method_not_allowed(self, request, *args, **kwargs): """用於設定當客戶端使用了不被支援的請求方式時應該如何處理 """ logger.warning( 'Method Not Allowed (%s): %s', request.method, request.path, extra={'status_code': 405, 'request': request} ) return HttpResponseNotAllowed(self._allowed_methods())
可以看到as_view裡面做了一個操作叫 self.kwargs = kwargs 、看到了希望!我的三觀還能保全、還要進一步求證這兩個self.kwargs是不是同一個
【找到關鍵】
1、在使用View時只能用as_view方式
urlpatterns = [ path('publishers/', PublisherList.as_view()), ]
2、我們並不直接使用SingleObjectMixin而是使用DetailView、而DetailView就給例項加上了self.kwargs = kwargs這一層邏輯
class BaseDetailView(SingleObjectMixin, View): """A base view for displaying a single object.""" def get(self, request, *args, **kwargs): self.object = self.get_object() context = self.get_context_data(object=self.object) return self.render_to_response(context) class SingleObjectTemplateResponseMixin(TemplateResponseMixin): template_name_field = None template_name_suffix = '_detail' class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView): """ Render a "detail" view of an object. By default this is a model instance looked up from `self.queryset`, but the view will support display of *any* object by overriding `self.get_object()`. """
可以看到BaseDetailView把SingleObjectMixin和View、也就是說第一個BaseDetailView的例項都會有kwargs屬性,這個原因可以追溯到
“刻苦學習在View.as_view方法中發現新的天地”這一節中提到的self.kwargs = kwargs。
由於View的例項都是由as_view建立的、在as_view的邏輯裡面就會給例項加上kwargs屬性、SingleObjectMixin.get_object中執行self.kwargs
自然就不會有錯誤了!
【DetailView相關的層次結構】
----