`basename` argument not specified, and could not automatically determine the name from the viewset, as it does not have a `.queryset` attribute.

前言

Django REST framework ( DRF )是一个强大且灵活的工具包,用于构建 Web API。DRF 有自己的一套路由定义方式,即通过 Router 类型的 register 方法,该方法包含了一个名为 basename 的参数,下面让我们通过了解这个参数来一窥 DRF 的路由系统吧!

探究哪些问题

为什么要写这个文章呢,因为在使用 Django REST framework 经常看到在配置路由的时候使用basename 这种用法,类似于 Django 中的 include 函数中的 namespace 参数和 path 中的 name 参数,那真相如何呢?请让我们带着下面三个问题来看这篇文章吧!👇

  • Django REST framework 中的 basenameDjango 中的 namespace 参数和name 参数有什么关系?
  • 什么时候需要加 basename ,什么时候不需要加 basename 呢?
  • 如果缺省 basename 参数,那 Django REST Framework 又会如何处理呢?

需要的预备知识

根据Django REST framework官方文档的介绍:Routers-英文文档 | Routers-中文文档

在解答第一个问题的时候,需要你先对router.register有所了解,具体可看这篇文章使用DRF的时候,选择router.register还是urlpatterns path ???

作用在哪:

打断点:

 

 

查看变量信息

 

 

可以发现,basename 的参数影响的是 URLPatternname 属性,这个 name 属性是拼接的

实际应用:

basename 主要是用在反向解析
一般流程是 url 路由->视图
但有的时候需要在视图中通过视图获取url
比如重定向,这个时候就可以通过 basename 做软编码

当 basename 缺省的时候

如果 basename 参数缺省,那么会使用 viewset 参数的 queryset 的属性,如果即不指定 basename 参数,又不指定 viewset 参数的 queryset 的属性 ,那么就会报错!

 File "C:\Users\17293\Desktop\Coder\Python\Django\twitter\twitter\urls.py", line 17, in <module>
    router.register('/api/accounts/', AccountViewSet)
  File "C:\Users\17293\AppData\Local\Programs\Python\Python39\lib\site-packages\rest_framework\routers.py", line 54, in register
    basename = self.get_default_basename(viewset)
  File "C:\Users\17293\AppData\Local\Programs\Python\Python39\lib\site-packages\rest_framework\routers.py", line 137, in get_default_basename
    assert queryset is not None, '`basename` argument not specified, and could ' \
AssertionError: `basename` argument not specified, and could not automatically determine the name from the viewset, as it does not have a `.queryset` attribute.

遇事不决看源码,一切的不解都可以再源码中解惑
Django REST framework 中的路由注册是这样子的

router = routers.SimpleRouter()
router.register('/api/accounts/', AccountViewSet, basename='accounts')

其中的 basename 参数可写可不写,不写的时候不代表就没有 basename 了,因为如前面所说,basename 是需要用来反向解析的,因此缺省 basename 会给一个默认值,让我们来看看 register 的源代码吧!

可以看到 register 方法的 basename 参数有一个默认值 None,但是这可不是最终的默认值哦!
从源码中可以看见当我们不自定义该参数的时候,默认为 None,当为 None 的时候,会从 viewset 参数中获取 basename 的值

class BaseRouter:
    def __init__(self):
        self.registry = []

    def register(self, prefix, viewset, basename=None):
        if basename is None:
            basename = self.get_default_basename(viewset)
        self.registry.append((prefix, viewset, basename))

        # invalidate the urls cache
        if hasattr(self, '_urls'):
            del self._urls

如果你有一个疑问:为什么我们在 urls.py 自定义的路由用的是 SimpleRouter 类的 register,为什么这里展示的源代码是 BaseRouter 类?答案是因为 SimpleRouter 类的 register 方法是从 BaseRouter 类继承来的, SimpleRouter 类并没有重写 register 方法

接下来看看如何获取默认的 basename,从源代码中可以看到 basename = self.get_default_basename(viewset) 是这句语句起了作用,那就观摩一下 self.get_default_basename 方法 👇

首先来看看 BaseRouter 类的 get_default_basename 方法,emmmm,这是一个抽象方法

def get_default_basename(self, viewset):
    """
    If `basename` is not specified, attempt to automatically determine
    it from the viewset.
    """
    raise NotImplementedError('get_default_basename must be overridden')

上面提到了一个概念:抽象方法

关于抽象方法,就是父类定义一个方法,你一调用这个方法就会抛出一个错误,哈哈,你是不是很奇怪,为什么要搞一个这个东西恶心人?调一下就报错?凭什么?为什么?干什么?
先回答为什么要定义这么一个恶心人的抽象方法之前,先来看看怎么用这个方法,抽象方法的正确打开方式是,在子类中继承父类,并且在子类中重写父类的这个抽象方法,即在子类中自定义个不会抛出错误的方法。
那为什么父类要搞这么一个抽象方法呢?因为这个抽象方法很重要(不是抽象方法很重要,是这个方法,这个被称为抽象方法,一调用就会抛出的出错的方法很重要,不是“抽象方法”这四个字或者“抽象方法”这四个字指代的那一类东西很重要!!!),既然是要重写才能用的,那不如直接不要在父类中定义了,直接写在子类中不好吗?也可以!完全可以!那为什么还要定义这个会抛出错误的方法呢?答案是占坑位,提醒写子类的人,别忘了这个重要方法!!!还有一点就是,你不继承的话,ide,诸如 vscode、pycharm 就会提示语法错误,毕竟你在 BaseRouter 类的 register 方法中调用了 get_default_basename 方法,结果你却没有定义什么是 get_default_basename ???那这些 ide 就要给你错误提示了!!!所以我们需要占一个坑位,定义一个假的 get_default_basename
总结:

  • 告诉要写子类的人,千万别忘了重写这个抽象方法(重要)
  • 占坑位,保证代码的完整性(不是那么重要)

再来看看 SimpleRouter 类的 get_default_basename 方法

def get_default_basename(self, viewset):
    """
    If `basename` is not specified, attempt to automatically determine
    it from the viewset.
    """
    queryset = getattr(viewset, 'queryset', None)

    assert queryset is not None, '`basename` argument not specified, and could ' \
        'not automatically determine the name from the viewset, as ' \
        'it does not have a `.queryset` attribute.'

    return queryset.model._meta.object_name.lower()

getattr(viewset, 'queryset', None) 语句先通过 getattr 函数获取 viewset 对象中的 queryset 属性,如果该属性不存在未返回 None,viewset 对象是什么?往回翻翻!

  • router.register('/api/accounts/', AccountViewSet, basename='accounts')
  • def register(self, prefix, viewset, basename=None):
  • basename = self.get_default_basename(viewset)

所以,viewset 对象是 AccountViewSet 类,这个类呢是不自带 queryset 属性的,需要人类,需要程序员,需要坐在你电脑前的你自己定义的

很简单对吧,但我想相信看这篇文章的有很多菜鸟,因此想讲的更入门一点,如果你不知道 getattr 函数是干嘛的?那你确实应该多去引擎搜索一下!什么?你连搜索引擎都不会使用!好吧,让我来告诉你搜索关键字,诸如 “python getattr 用法” 这样的词条就可以找你到你想要的答案了

接着往下看到断言语句 assert ,如果 queryset is not None ,就 pass ,如果 queryset 是 None ,那就要抛出错误了!

什么!你连断言 assert 都不知道!!!你简直就是一个 🤡

这个时候就很明白了,如果你既没有自定义 basename 属性,又不定义 AccountViewSet 类的 queryset 属性,就会报错和你说拜拜

那如果是没有自定义 basename 属性,但是定义了 AccountViewSet 类的 queryset 属性呢?这个时候会就使用 queryset 类作为 basename ,但是有一个小问题,就是 basename 是一个字符串,而 queryset 往往是一个 QuerySet 类,或者是一个 models.Model 的子类,显然类不能当作字符串呀,这个时候就还需要进一步处理!接着往下看吧!

queryset.model._meta.object_name.lower()

可以看下 .lower() 方法,这是字符串类型的内置方法,负责将字符串转为小写字符, object_name 就是一个字符串对象,该对象保存着 AccountViewSet 类的名字,在转成小写,就变成了 accountviewset

不懂 lower 方法的,请参考 Python3 lower()方法
posted @ 2021-07-05 18:39  Jerry`  阅读(779)  评论(5编辑  收藏  举报