基于类的通用视图:ListView 和 DetailView

在开发网站的过程中,有一些视图函数虽然处理的对象不同,但是其大致的代码逻辑是一样的。比如一个博客和一个论坛,通常其首页都是展示一系列的文章列表或者帖子列表。对处理首页的视图函数来说,虽然其处理的对象一个是文章,另一个是帖子,但是其处理的过程是非常类似的。首先是从数据库取出文章或者帖子列表,然后将这些数据传递给模板并渲染模板。于是,Django 把这些相同的逻辑代码抽取了出来,写成了一系列的通用视图函数,即基于类的通用视图(Class Based View)。

使用类视图是 Django 推荐的做法,而且熟悉了类视图的使用方法后,能够减少视图函数的重复代码,节省开发时间。接下来就让我们把博客应用中的视图函数改成基于类的通用视图。

ListView

在我们的博客应用中,有几个视图函数是从数据库中获取文章(Post)列表数据的:

blog/views.py

def index(request):
    # ...

def archives(request, year, month):
    # ...

def category(request, pk):
    # ...

这些视图函数都是从数据库中获取文章(Post)列表,唯一的区别就是获取的文章列表可能不同。比如 index 获取全部文章列表,category 获取某个分类下的文章列表。

将 index 视图函数改写为类视图

针对这种从数据库中获取某个模型列表数据(比如这里的 Post 列表)的视图,Django 专门提供了一个 ListView 类视图。下面我们通过一个例子来看看 ListView 的使用方法。我们首先把 index 视图函数改造成类视图函数。

blog/views.py

from django.views.generic import ListView

class IndexView(ListView):
    model = Post
    template_name = 'blog/index.html'
    context_object_name = 'post_list'

要写一个类视图,首先需要继承 Django 提供的某个类视图。至于继承哪个类视图,需要根据你的视图功能而定。比如这里 IndexView 的功能是从数据库中获取文章(Post)列表,ListView 就是从数据库中获取某个模型列表数据的,所以 IndexView 继承 ListView

然后就是通过一些属性来指定这个视图函数需要做的事情。这里我们指定了三个属性。

  • model。将 model 指定为 Post,告诉 Django 我要获取的模型是 Post
  • template_name。指定这个视图渲染的模板。
  • context_object_name。指定获取的模型列表数据保存的变量名。这个变量会被传递给模板。

如果还是有点难以理解,不妨将类视图的代码和 index 视图函数的代码对比一下:

blog/views.py

def index(request):
    post_list = Post.objects.all()
    return render(request, 'blog/index.html', context={'post_list': post_list})

index 视图函数首先通过 Post.objects.all() 从数据库中获取文章(Post)列表数据,并将其保存到 post_list 变量中。而在类视图中这个过程 ListView 已经帮我们做了。我们只需告诉 ListView 去数据库获取的模型是 Post,而不是 Comment 或者其它什么模型,即指定 model = Post。将获得的模型数据列表保存到 post_list 里,即指定 context_object_name = 'post_list'。然后渲染 blog/index.html 模板文件,index 视图函数中使用 render 函数。但这个过程 ListView 已经帮我们做了,我们只需指定渲染哪个模板即可。

接下来就是要将类视图转换成函数视图。为什么需要将类视图转换成函数视图呢?

我们来看一看 blog 的 URL 配置:

blog/urls.py

app_name = 'blog'
urlpatterns = [
    url(r'^$', views.index, name='index'),
    ...
]

前面已经说过每一个 URL 对应着一个视图函数,这样当用户访问这个 URL 时,Django 就知道调用哪个视图函数去处理这个请求了。在 Django 中 URL 模式的配置方式就是通过 url 函数将 URL 和视图函数绑定。比如 url(r'^$', views.index, name='index'),它的第一个参数是 URL 模式,第二个参数是视图函数 index。对 url 函数来说,第二个参数传入的值必须是一个函数。而 IndexView 是一个类,不能直接替代 index 函数。好在将类视图转换成函数视图非常简单,只需调用类视图的 as_view() 方法即可(至于 as_view 方法究竟是如何将一个类转换成一个函数的目前不必关心,只需要在配置 URL 模式是调用 as_view 方法就可以了。具体的实现我们以后会专门开辟一个专栏分析类视图的源代码,到时候就能看出 Django 使用的魔法了)。

现在在 URL 配置中把 index 视图替换成类视图 IndexView

blog/urls.py

app_name = 'blog'
urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    ...
]

访问一下首页,可以看到首页依然显示全部文章列表,和使用视图函数 index 时效果一模一样。

将 category 视图函数改写为类视图

category 视图函数的功能也是从数据库中获取文章列表数据,不过其和 index 视图函数不同的是,它获取的是某个分类下的全部文章。因此 category 视图函数中多了一步,即首先需要根据从 URL 中捕获的分类 id 并从数据库获取分类,然后使用 filter 函数过滤出该分类下的全部文章。来看看这种情况下类视图该怎么写:

blog/views.py

class CategoryView(ListView):
    model = Post
    template_name = 'blog/index.html'
    context_object_name = 'post_list'

    def get_queryset(self):
        cate = get_object_or_404(Category, pk=self.kwargs.get('pk'))
        return super(CategoryView, self).get_queryset().filter(category=cate)

IndexView 不同的地方是,我们覆写了父类的 get_queryset 方法。该方法默认获取指定模型的全部列表数据。为了获取指定分类下的文章列表数据,我们覆写该方法,改变它的默认行为。

首先是需要根据从 URL 中捕获的分类 id(也就是 pk)获取分类,这和 category 视图函数中的过程是一样的。不过注意一点的是,在类视图中,从 URL 捕获的命名组参数值保存在实例的 kwargs 属性(是一个字典)里,非命名组参数值保存在实例的 args 属性(是一个列表)里。所以我们使了 self.kwargs.get('pk') 来获取从 URL 捕获的分类 id 值。然后我们调用父类的 get_queryset 方法获得全部文章列表,紧接着就对返回的结果调用了 filter 方法来筛选该分类下的全部文章并返回。

此外我们可以看到 CategoryView 类中指定的属性值和 IndexView 中是一模一样的,所以如果为了进一步节省代码,甚至可以直接继承 IndexView

class CategoryView(IndexView):
    def get_queryset(self):
        cate = get_object_or_404(Category, pk=self.kwargs.get('pk'))
        return super(CategoryView, self).get_queryset().filter(category=cate)

然后就在 URL 配置中把 category 视图替换成类视图 CategoryView

blog/urls.py

app_name = 'blog'
urlpatterns = [
    ...
    url(r'^category/(?P<pk>[0-9]+)/$', views.CategoryView.as_view(), name='category'),
]

访问以下某个分类页面,可以看到依然显示的是该分类下的全部文章列表,和使用视图函数category 时效果一模一样。

将 archives 视图函数改写成类视图

这里没有什么新东西要讲了,学以致用,这个任务就交给你自己了。如果还是不会写的请参考本章节的 GitHub 代码 Step18: class based views

DetailView

除了从数据库中获取模型列表的数据外,从数据库获取模型的一条记录数据也是常见的需求。比如查看某篇文章的详情,就是从数据库中获取这篇文章的记录然后渲染模板。对于这种类型的需求,Django 提供了一个 DetailView 类视图。下面我们就来将 detail 视图函数转换为等价的类视图 PostDetailView,代码如下:

blog/views.py

from django.views.generic import ListView, DetailView

# 记得在顶部导入 DetailView
class PostDetailView(DetailView):
    # 这些属性的含义和 ListView 是一样的
    model = Post
    template_name = 'blog/detail.html'
    context_object_name = 'post'

    def get(self, request, *args, **kwargs):
        # 覆写 get 方法的目的是因为每当文章被访问一次,就得将文章阅读量 +1
        # get 方法返回的是一个 HttpResponse 实例
        # 之所以需要先调用父类的 get 方法,是因为只有当 get 方法被调用后,
        # 才有 self.object 属性,其值为 Post 模型实例,即被访问的文章 post
        response = super(PostDetailView, self).get(request, *args, **kwargs)

        # 将文章阅读量 +1
        # 注意 self.object 的值就是被访问的文章 post
        self.object.increase_views()

        # 视图必须返回一个 HttpResponse 对象
        return response

    def get_object(self, queryset=None):
        # 覆写 get_object 方法的目的是因为需要对 post 的 body 值进行渲染
        post = super(PostDetailView, self).get_object(queryset=None)
        post.body = markdown.markdown(post.body,
                                      extensions=[
                                          'markdown.extensions.extra',
                                          'markdown.extensions.codehilite',
                                          'markdown.extensions.toc',
                                      ])
        return post

    def get_context_data(self, **kwargs):
        # 覆写 get_context_data 的目的是因为除了将 post 传递给模板外(DetailView 已经帮我们完成),
        # 还要把评论表单、post 下的评论列表传递给模板。
        context = super(PostDetailView, self).get_context_data(**kwargs)
        form = CommentForm()
        comment_list = self.object.comment_set.all()
        context.update({
            'form': form,
            'comment_list': comment_list
        })
        return context

PostDetailView 稍微复杂一点,主要是等价的 detail 视图函数本来就比较复杂,下面来一步步对照 detail 视图函数中的代码讲解。

首先我们为 PostDetailView 类指定了一些属性的值,这些属性的含义和 ListView 中是一样的,这里不再重复讲解。

紧接着我们覆写了 get 方法。这对应着 detail 视图函数中将 post 的阅读量 +1 的那部分代码。事实上,你可以简单地把 get 方法的调用看成是 detail 视图函数的调用。

接着我们又复写了 get_object 方法。这对应着 detail 视图函数中根据文章的 id(也就是 pk)获取文章,然后对文章的 post.body 进行 Markdown 渲染的代码部分。

最后我们复写了 get_context_data 方法。这部分对应着 detail 视图函数中生成评论表单、获取 post 下的评论列表的代码部分。这个方法返回的值是一个字典,这个字典就是模板变量字典,最终会被传递给模板。

你也许会被这么多方法搞乱,为了便于理解,你可以简单地把 get 方法看成是 detail 视图函数,至于其它的像 get_objectget_context_data 都是辅助方法,这些方法最终在 get 方法中被调用,这里你没有看到被调用的原因是它们隐含在了 super(PostDetailView, self).get(request, *args, **kwargs) 即父类 get 方法的调用中。最终传递给浏览器的 HTTP 响应就是 get 方法返回的 HttpResponse 对象。

还是无法理解么?在不涉及源码的情况下我也只能讲这么多了。要想熟练掌握并灵活运用类视图必须仔细阅读类视图的源码,我当时也是啃源码啃了很久很久,以后我会专门开辟一个专题分析类视图的源码,到时候你就会对类视图有更深的理解了。此外,这里是 Django 官方文档对类视图的讲解,尽管我觉得这部分文档对类视图也讲得不是很清楚,不过也值得作为参考吧 基于类的视图概述

总结

本章节的代码位于:Step18: class based views

如果遇到问题,请通过下面的方式寻求帮助。

  • 在下方评论区留言。
  • 将问题的详细描述通过邮件发送到 djangostudyteam@163.com,一般会在 24 小时内回复。
  • Pythonzhcn 社区的新手问答版块 发布帖子。

-- EOF --


67 条评论 / 30 人参与
ask8xm

偷偷帖一下get函数的里面东西,调用了self.get_object() 和 get_context_data()

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)

fuweifu-vtoo/jonathan

博主你好,使用类的通用视图之后,如何对post_list按照created_time逆排序呢?(不管正逆,总之我想加上排序的功能)

我有尝试过在get_context_data()函数中复写更新post_list,但是这样会对分页有影响。

谢谢博主的解答!!!!!!


fuweifu-vtoo/jonathan fuweifu-vtoo/jonathan

看后面的评论解决了~~~

在Indexview中复写下面的方法:

def get_queryset(self):         

return super(IndexView, self).get_queryset().order_by('-created_time')


WCHONGE

博主 你好 改写成 DetailVIew 后 会有  'Post' object has no attribute 'increase_view' 报错,去掉self.object.increase_view()后能够正常显示 但不能统计阅读量了 可能是什么问题呢

def get(self, request, *args, **kwargs):
response = super(PostDetailView, self).get(request, *args, **kwargs)
self.object.increase_view()
return response

lihaoingsky WCHONGE

你检查一下你Models里面  Post 是否有increase_view()这个方法。很可能你增加阅读量的方法,不是这个名字。


海豹不是豹

博主你好!

代码注释中有提到过   “之所以需要先调用父类的 get 方法,是因为只有当 get 方法被调用后, # 才有 self.object 属性,其值为 Post 模型实例,即被访问的文章 post” ,那这个self.object返回的对象与get_object()返回的对象是同一个吗?

还有,上面说道    “像 get_object、get_context_data 都是辅助方法,这些方法最终在 get 方法中被调用,这里你没有看到被调用的原因是它们隐含在了 super(PostDetailView, self).get(request, *args, **kwargs) 即父类 get 方法的调用中。”   那可不可以将复写在get_object、get_context_data方法里的代码都通过self.object写在get函数里?

谢谢!


追梦人物 [博主] 海豹不是豹

理论上是可以的,但就丧失了通用视图的优势,看你具体的需求是什么了。


海豹不是豹 追梦人物 [博主]

好的,谢谢博主!


omucc

def get_object(self, queryset=None): 

 # 覆写 get_object 方法的目的是因为需要对 post 的 body 值进行渲染

对post的body进行渲染不可以放到后面复写的get_context_data里面吗?放到这里的好处是什么,或者为什么只能放到get_object里面。。没想明白。。这里先感谢博主回复了


追梦人物 [博主] omucc

其实也是可以的,只要保证返回response前被渲染了就可以,只是我认为放在 get_object 最为合理


EvenH

你好,为什么我是commet_set报错


EvenH EvenH

已解决错误,感觉自己智障了一下


qzuser50752 EvenH

请问怎么解决的呢?我报错和你一样


双鱼幸福leon

你好,看了实例怎么感觉类视图更复杂了呢?能阐明优势在哪吗


追梦人物 [博主] 双鱼幸福leon

优势就是可以写更少的代码


双鱼幸福leon 追梦人物 [博主]

怎么感觉比之前代码量多了呢。不过后面在继承分页时的确简单了很多


silencehero

博主改为detailview报这个错,No module named 'mdx_markdown' 


追梦人物 [博主] silencehero

我认为你 markdown 应该装错了。


Zeng1998

class CategoryView(ListView):
model = Post
template_name = 'blog/index.html'
context_object_name = 'post_list'


def get_queryset(self):
cate = get_object_or_404(Category, pk=self.kwargs.get('pk'))
return super(CategoryView, self).get_queryset().filter(category=cate)

博主,这段代码能不能直接改成

class CategoryView(ListView):
model = Post
template_name = 'blog/index.html'
context_object_name = 'post_list'


def get_queryset(self):
return super(CategoryView, self).get_queryset().filter(pk=self.kwargs.get('pk'))

感觉效果一样啊 为什么要多那一步找出对应的分类


追梦人物 [博主] Zeng1998

看上去效果并不等价哦,实现的功能是筛选该分类下的全部文章。修改后并没有筛选分类下的文章。


哈哈餐馆

关于改写 ListView 有个问题想请教大家和博主大人。

我使用的版本是 Django 1.11.7 然后之前看过1.11 的官方入门教程,如果按照官方的改写方法,以 CategoryView 为例吧,大概会写成这样:

class CategoryView(generic.ListView):
model = Post
template_name = 'blog/index.html'
context_object_name = 'post_list'

def get_queryset(self):
cate = get_object_or_404(Category, pk=self.kwargs.get('pk'))
return Post.objects.filter(category=cate)

最后一句的返回写法和博主教程的不一样

return super(CategoryView, self).get_queryset().filter(category=cate)

这两种方法在前端看起来页面都是一样的。


我想请问:这两种方法在后端或者说 Python 层面来看是一样的么?(比如说效率什么的)那种更好?在我小白看来似乎官方写法更省字


zlh95 哈哈餐馆

官方的写法容易看懂一些,他们做的都是同一件事情,获取所有文章,然后过滤。


point6013

博主,我有一个问题想请教,在通用视图中,DetailView中我想向模板中添加其他的内容,而且该内容是根据post内容通过调用其他方法生成的,添加时间,我已经完成,在get_context_data中可以实现,但是其他的内容如何添加,也就是说在get_context_data中如何调用request和pk。或者有没有其他方案?烦请指导。


追梦人物 [博主] point6013

你可以使用 self.object 获取到和这个 view 关联的 post 对象,例如self.object.pk。self.request 获取到关联的 http 请求对象。


point6013 追梦人物 [博主]

回去测试一下,非常感谢您的教程,慢慢开始入门django了


zlh95 追梦人物 [博主]

意思是在get()方法里,用self.object.post对象的方法把?


追梦人物 [博主] zlh95

这里 self.object 就是获取的 post


fshgrym

多写几遍就理解了哈


igmpxb

有一个问题呢

在没有用试图类之前我们是这样获取文章列表的,这个是按时间·排序了的

post_list = Post.objects.all().order_by('-creater_time')


可是

 class IndexView(ListView):

model = Post

template_name = 'blog/index.html'

context_object_name = 'post_list'

这样以后就没有按时间排序的效果了= =。 


大家都是怎么弄的呢


igmpxb igmpxb

晕 这个问题我在往后面看的时候解决了,解决的方法是重写get_queryset该方法,目测是可行的,大家看看这样解有问题吗?

def get_queryset(self):

return super(IndexView, self).get_queryset().order_by('-creater_time')


大圣______ igmpxb

前面的教程里,博主写在Post模块里了


fuweifu-vtoo/jonathan igmpxb

created_time 写成了 creater_time,谢谢你的评论!


luoliang

大佬,为什么我按着你代码敲的,改成视图函数后PostDetailView。每次点一个文章,阅读数怎么没保存上?


追梦人物 [博主] luoliang

show me the code.


荧惑1516

博主你好,我换成了mysql数据库,但是这样导致了明明有文章,归档也有显示时间,但是点击时间后显示没有文化在那个提示,换回sqlite就好了,而且只有归档有bug,其他没有,请问怎么解决啊


追梦人物 [博主] 荧惑1516

好像是 mysql 的时间存储的缘故,评论区里找一找,好像有人提过解决方案


荧惑1516 追梦人物 [博主]

好的,谢谢博主


hesiyu

博主你好,我CategoryView继承了IndexView,但是我想让index和category都支持markdown,怎么把get_queryset整合成一个,或者直接从IndexView继承过来呢?

class CategoryView(IndexView):
def get_queryset(self):
cate = get_object_or_404(Category, pk=self.kwargs.get('pk'))
return super(CategoryView, self).get_queryset().filter(category=cate)

def get_queryset(self):
post_list = Post.objects.all().order_by('-created_time')
for post in post_list:
post.excerpt = markdown.markdown(post.excerpt,
extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
])
return post_list

追梦人物 [博主] hesiyu

可以继承呀!


王_小_花_

博主你好,我有一个疑问:

DetailView中用来获取指定post的那行代码post = super(PostDetailView, self).get_object(queryset=None),并没有传入关于pk的参数,是怎么按照文章的pk查找的?另外,queryset的作用是什么?


mamami

找到错误了,IndexView我把它定义成函数了


mamami

from django.views.generic import ListView,这个我的导入了,但是显示没有使用,显示错误:

AttributeError: 'function' object has no attribute 'as_view' ,

找到ListView源代码,显示只有class ListView(MultipleObjectTemplateResponseMixin, BaseListView):

里头啥也没有


dackzome
大佬,使用完新的通用视图类,我发现首页的评论数量消失了,不能统计数量。没改新的视图之前,是能正常显示的。是否因为在class IndexView中,只传了post_list的缘故?应该怎么做才可以让他继续显示正常?

追梦人物 [博主] dackzome
这看你代码怎么写的,评论数量可以使用类似于 post.comment_set.count() 的代码获取的。你可以通过视图函数传给模板,也可以直接在模板中调用。

dackzome 追梦人物 [博主]
{{comment_list.count}} 评论这是之前的,在detail.html中可以正常显示,但是在index.html中不能正常显示。现在在index.html中用了 {{post.comment_set.count}} 评论,index.html能正常显示了,感谢博主。

科兴一把刀
博主,PostDetailView那里我试了一下,不复写get函数,直接把阅读量+1这个函数放到get_object里面也可以:
def get_object(self, queryset=None):
post = super(PostDetailView, self).get_object(queryset=None)
# 阅读量+1
post.increase_views()
# 其他代码...
return post
能否说说get和get_object的区别呢?

追梦人物 [博主] 科兴一把刀
可能是因为 get 方法中调用了 get_object 方法,推荐放在 get 中,这样更加直观。

科兴一把刀 追梦人物 [博主]
嗯,这样思路是比较清晰

知而不倦
感谢博主,你讲得很好的,至少是我目前看到过最好的教材,内容也丰富,而且通俗易懂。刚接触django感觉好难,直接看官方中文翻译文章看的云里雾里,根本不知道讲什么,而且文档太长,我觉得有些的功能也很少用到的,也不知道重点,文档虽然很全,但是感觉讲的好乱。后来看的一些入门教材,能看懂,感觉讲的内容太少的,也没有结合实际项目实践。后面看了一些视频,对django的主要功能有些了解,视频不好笔记,也不好复习,过段时间又忘了,主要是自己没有练吧!跟着你的教程一步步来,还有源码的。有些感觉的。真的感谢。可以的话也可以写本书的,flask有本书感觉还不错,感觉市场上django的好书太少,之前买了一本挺厚的,翻翻就不想看了,干货太少废话连篇,讲的也很基础,还不如网上的入门教材。就不说是哪本书了。

追梦人物 [博主] 知而不倦
谢谢你的回复,希望教程对大家有帮助。django 开发的 90% 的东西都在文档中,文档是基础。但是如你所说,文档全面但是零散。所以我就写一些实战项目教程,作为文档的补充。也帮助大家在实战中学习开发 django。

严健菊_144
class base():
def __init__(self):
#下面的index和show方法都传值data,我想在__init__这边就要传递,有什么方法不?
pass

def index(self,request):
data = 1
return render(request,'index.html',{'data':data})

def show(self,request):
data = 1
return render(request,'show.html',{'data':data})

追梦人物 [博主] 严健菊_144
没有明白你的意思,你的意思是这样么?def __init__(self, data=1)

sakura1357
博主报错: ImproperlyConfigured at /post/4/
DetailView is missing a QuerySet. Define DetailView.model, DetailView.queryset, or override DetailView.get_queryset().
Request Method: GET
Request URL: http://127.0.0.1:8000/post/4/
Django Version: 1.10.6
Exception Type: ImproperlyConfigured
Exception Value:
DetailView is missing a QuerySet. Define DetailView.model, DetailView.queryset, or override DetailView.get_queryset().

提示没有QuerySet,我看你的代码POSTDetailView里面没有复写get_queryset()方法

sakura1357 sakura1357
我在PostDetailView里面复写了这个方法之后还是报错:
‘’‘python
def get_queryset(self):
return super(PostDetailView, self).get_queryset()
'''

追梦人物 [博主] sakura1357
类属性设置的和教程中的一致么?

sakura1357 追梦人物 [博主]
一样的,都是这3个属性,我看了你在github上的本章节的源码,一模一样的代码。

追梦人物 [博主] sakura1357
如果代码和项目中的一样,那就不知道是哪里的问题了。

mamami sakura1357

blog/urls里你视图函数应该写错了,应该是views.PostDetailView.as_view(),我跟你错的一样的错,就是因为我把视图函数写成了views.DetailView.as_view()


JustBreaking
评论不支持删除功能呀,博主可以加上
另外我想问的是 detailsView类中, urls 中的文章pk参数是怎么传进去的?

追梦人物 [博主] JustBreaking
django 帮我们自动把从 url 捕获的参数保存在了 self.kwargs 属性里。

JustBreaking JustBreaking
好吧 ,跟着博主写了一遍 ,豁然开朗,多谢多谢!!

JustBreaking 追梦人物 [博主]
嗯 教程里面叙述了,我开始还没开完教程就开始问。习惯不好O(∩_∩)O~

JustBreaking
如果要传给模板的数据不止post_list,这种方法还能用么?

追梦人物 [博主] JustBreaking
你可以在 get_context 方法中插入任何你想传递的模板变量呀。

JustBreaking 追梦人物 [博主]
嗯 明白了 多谢

李伟
说实话类视图到DetailView的时候有点晕了。

追梦人物 [博主] 李伟
慢慢消化吧,确实不好理解。实在无法理解就还是用函数写法。