`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
中的basename
和Django
中的namespace
参数和name
参数有什么关系?- 什么时候需要加
basename
,什么时候不需要加basename
呢? - 如果缺省
basename
参数,那Django REST Framework
又会如何处理呢?
需要的预备知识
根据Django REST framework
官方文档的介绍:Routers-英文文档 | Routers-中文文档
在解答第一个问题的时候,需要你先对router.register有所了解,具体可看这篇文章使用DRF的时候,选择router.register还是urlpatterns path ???
作用在哪:
打断点:
查看变量信息
可以发现,basename
的参数影响的是 URLPattern
的 name
属性,这个 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()方法