Python / 10 Django 中传递参数给视图函数

Django 中传递参数给视图函数

Django 框架中推荐使用一个单独的 python 模块配置 URL 和视图函数或者视图类的映射关系,通常称这个配置模块为 URLconf,该 Python 模块通常命名为 urls.py。一般而言,每个应用目录下都会有一个 urls.py 文件,总的映射关系入口在项目目录下的 urls.py 中,而这个位置又是在 settings.py 文件中指定的。

本小节中将会学习 Django 中的路由系统、URLconf 的配置,以及如何将请求参数,如表单数据、文件等传递给视图函数。

1. 测试环境准备

这里的实验环境会采用前面创建的第一个 Django 工程(first_django_app) 来进行测试。在 first_django_app 中,我们创建了第一个 django app 应用:hello_app。现在按照如下步骤准备实验环境:

在 hello_app 应用目录下的 views.py 中添加一个视图函数:

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.

def hello_view(request, *args, **kwargs):
    return HttpResponse('hello, world!')

在 hello_app 应用目录下新建 urls.py 文件,里面内容如下:

from django.urls import path

from . import views

urlpatterns = [
    # 资产查询接口,根据我们自己的机器命名
    path('world/', views.hello_view, name='hello_view'),
]

settings.py 文件中注册应用:

# first_django_app/settings.py
...

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 注册应用
    'hello_app'
]

...

在 URLconf 的总入口位置使用 django 提供的 include 方法将应用中的 urls.py 中的 urlpatterns 添加进来:

from django.contrib import admin
from django.conf.urls import include, url

# 所有url入口
urlpatterns = [
    url('admin/', admin.site.urls),
    url('hello/', include('hello_app.urls')),
]

这样之后,我们请求地址 /hello/world/ 时,就能看到页面显示 “hello, world!” 字符了。后面的测试将在应用的 urls.pyviews.py 中进行。

2. Django 中的传参方式

Django 中传递参数给视图函数的方式主要可分为以下两种形式:URL 传参和非 URL 传参两种。第一种基于 Django 中的 URLconf 配置,可以通过 URL 路径将对应匹配的参数传到视图函数中;而另外一种就是属于HTTP 请求携带的参数了,请求参数可以放到 URL 中以 格式加到 URL 的最后面,也可以将参数放到请求 body 中,最后统一由视图函数中的 request 参数保存并传到视图函数中。

2.1 动态 URL 传参

在 url 的路径 (path)部分可以作为动态参数,传递给视图函数,如下面几种写法:

# hello_app/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('articles/<int:year>/', views.year_archive),
    path('articles/<int:year>/<int:month>/', views.month_archive),
    path('articles/<int:year>/<int:month>/<slug:title>/', views.article_title),
]

注意上面的定义了匹配三个动态 URL 的映射,每个动态 URL 会匹配一个至多个参数,每个动态值使用 <> 符号匹配,采用 <type:name> 这样的形式。我们对应的视图函数如下:

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.

def year_archive(request, year, *args, **kwargs):
    return HttpResponse('hello, {} archive\n'.format(year))
    
def month_archive(request, year, month, *args, **kwargs):
    return HttpResponse('hello, month archive, year={}, moth={}!\n'.format(year, month))

def article_title(request, year, month, title, *args, **kwargs):
    return HttpResponse('hello, title archive, year={}, month={}, title={}!\n'.format(year, month, title))

对于动态的 URL 表达式中,匹配到的值,比如上面的 year,month 和 title 可以作为函数的参数放到对应的视图函数中,Django 会帮我们把匹配到的参数对应的放到函数的参数上。这里参数的位置可以任意写,但是名字必须和 URL 表达式中的对应。

[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, 1998 archive
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/
hello, month archive, year=1998, moth=12!
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/test/
hello, title archive, year=1998, month=12, title=test

比如 URL 中有 3 个动态参数,在视图函数中只写上两个参数接收也是没问题的,因为剩下的参数会被传到 kwargs 中以 key-value 的形式保存:

(django-manual) [root@server first_django_app]# cat hello_app/views.py
...

def article_title(request, year, month, *args, **kwargs):
    return HttpResponse('hello, title archive, year={}, month={}, kwargs={}\n'.format(year, month, kwargs))

# 启动服务,再次请求后
[root@server first_django_app]# curl http://127.0.0.1:8881/hello/articles/1998/12/test/
hello, title archive, year=1998, month=12, kwargs={'title': 'test'}

上述介绍的动态 URL 匹配格式 <type:name> 中,Django 会对捕捉到的 URL 参数进行强制类型装换,然后赋给 name 变量,再传到视图函数中。其中 Django 框架中支持的转换类型有:

  • str:匹配任意非空字符,不能匹配分隔符 “/”;

  • int:匹配任意大于0的整数;

  • slug:匹配任意 slug 字符串, slug 字符串可以包含任意的 ASCII 字符、数字、连字符和下划线等;

  • uuid:匹配 UUID 字符串;

  • path:匹配任意非空字符串,包括 URL 的分隔符 “/”。

2.2 自定义URL参数类型转换器

除了 Django 定义的简单类型,我们还可以自定义参数类型转换器来支持更为复杂的 URL 场景。比如前面的 int 类型并不支持负整数,我希望开发一个能匹配正负数的类型,具体的步骤如下:

hello_app/urls.py 中定义一个 SignInt 类。该类有一个固定属性 regex,用于匹配动态值;两个固定方法:to_python() 方法和 to_url() 方法:

# hello_app/urls.py

class SignInt:
    regex = '-*[0-9]+'

    def to_python(self, value):
        # 将匹配的value转换成我们想要的类型
        return int(value)

    def to_url(self, value):
        # 反向生成url时候回用到
        return value

注册该定义的转换器,并给出一个简短的名字,比如 sint:

# hello_app/urls.py

from django.urls import converters, register_converter

register_converter(SignInt, 'sint')

...

最后,我们就可以在 URLconf 中使用该类型来配置动态的 URL 表达式:

# hello_app/urls.py

urlpatterns = [
    path('articles/<sint:signed_num>/', views.signed_convert),
]

# hello_app/views.py
def signed_convert(request, signed_num, **kwargs):
    return HttpResponse('hello, 自定义类型转换器,获取参数={}\n'.format(signed_num))

启动 Django 服务后,执行相关请求,可以看到 Django 的路由系统能成功匹配到带负数和正数的 URL:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, 自定义类型转换器,获取参数=1998
[root@server ~]# curl http://127.0.0.1:8881/hello/articles/-1998/
hello, 自定义类型转换器,获取参数=-1998

2.3 使用正则表达式

上面是比较简单的 URLconf 配置形式,Django 框架中可以使用正则表达式来进一步扩展动态 URL 的配置,此时 urlpatterns 中的不再使用 path 方法而是支持正则表达式形式的 re_path 方法。此外,在 Python 的正则表达式中支持对匹配结果进行重命名,语法格式为:(?P<name>pattern),其中 name 为该匹配的名称,pattern 为匹配的正则表达式。 这样我们可以有如下的 URLconf 配置:

# hello_app/urls.py
from django.urls import re_path

from . import views

urlpatterns = [
    re_path('articles/(?P<year>[0-9]{4})/', views.year_archive),
    re_path('articles/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/', views.month_archive),
    re_path('articles/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/(?P<title>[a-zA-Z0-9-_]+)/', views.article_title),
]

注意:这里使用正则表达式的 URL 匹配和前面的普通的动态 URL 匹配有一个非常重要的区别,基于正则表达式的URL 匹配一旦匹配成功就会直接跳转到视图函数进行处理,而普通的动态 URL 匹配则会找到最长匹配的动态 URL,然后再进入相应的视图函数去处理:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/12/test
hello, 1998 archive

可以看到,这里并没有匹配到第三个 re_path 的 URL 配置,而是直接由第一个 re_path 的视图函数进行了处理。

2.4 URLconf 传递额外参数

在前面的 URLconf 配置中,我们的 re_path 方法中只传递两个参数,分别是设计的路由以及对应的视图函数。我们可以看看 Django-2.2.10 中的 path 和 re_path 方法的源代码:

# django/urls/conf.py
# ...

def _path(route, view, kwargs=None, name=None, Pattern=None):
    if isinstance(view, (list, tuple)):
        # For include(...) processing.
        pattern = Pattern(route, is_endpoint=False)
        urlconf_module, app_name, namespace = view
        return URLResolver(
            pattern,
            urlconf_module,
            kwargs,
            app_name=app_name,
            namespace=namespace,
        )
    elif callable(view):
        # view是函数
        pattern = Pattern(route, name=name, is_endpoint=True)
        return URLPattern(pattern, view, kwargs, name)
    else:
        raise TypeError('view must be a callable or a list/tuple in the case of include().')


path = partial(_path, Pattern=RoutePattern)
re_path = partial(_path, Pattern=RegexPattern)

可以看到,除了route 和 view 外,我们还有 name、kwargs、Pattern 参数(比较少用)。其中 name 参数表示的是 route 匹配到的 URL 的一个别名,而 kwargs 是我们可以额外传给视图函数的参数:

# hello_app/urls.py
...

urlpatterns = [
    re_path('articles/(?P<year>[0-9]{4})/', views.year_archive, {'hello': 'app'}),
]

# hello_app/views.py
def year_archive(request, *args, **kwargs):
    return HttpResponse('hello, year archive, 额外参数={}\n'.format(kwargs))

启动 Django 服务后,我们请求对应的服务,可以看到除了 URL 中匹配的 year 参数外,还有 re_path 中额外传递的参数,最后都被视图函数中的 **kwargs 接收:

[root@server ~]# curl http://127.0.0.1:8881/hello/articles/1998/
hello, year archive, 额外参数={'year': '1998', 'hello': 'app'}

2.5 从 HttpRequest 中获取参数

从 HttpRequest 中获取参数是我们进行 Web 开发中最常用的一种方式。对于 Django 的视图函数来说,HTTP 请求的数据被 HttpRequest 实例化后传到了视图函数的第一个参数中。为了能观察相关信息,我们修改请求的视图函数:

@csrf_exempt
def hello_view(request, *args, **kwargs):
    # 在第三次使用表单上传包括文件数据时,需要request.GET和request.POST操作,不然会抛异常
    params = "request.GET={}\n".format(request.GET)
    params += "request.POST={}\n".format(request.POST)
    params += "request.body={}\n".format(request.body)
    params += "request.FILES={}\n".format(request.FILES)

    return HttpResponse(params)

我们测试如下 3 种 HTTP 请求,分别为 GET 请求、POST 请求 和带文件参数的请求,结果如下:

[root@server ~]# curl -XGET "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" 
request.GET=<QueryDict: {'a': ['xxxx'], 'b': ['yyyy']}>
request.POST=<QueryDict: {}>
request.body=b''
request.FILES=<MultiValueDict: {}>

[root@server ~]# curl -XPOST -d "username=shen&password=shentong" "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" 
request.GET=<QueryDict: {'a': ['xxxx'], 'b': ['yyyy']}>
request.POST=<QueryDict: {'username': ['shen'], 'password': ['shentong']}>
request.body=b'username=shen&password=shentong'
request.FILES=<MultiValueDict: {}>

# 本次请求中,需要去掉request.GET和request.POST操作语句,不然请求会报错
[root@server ~]# curl -XPOST -F "username=shen&password=shentong" "http://127.0.0.1:8881/hello/world/?a=xxxx&b=yyyy" -F "files=@/root/upload_file.txt"
request.body=b'------------------------------68c9ede00e93\r\nContent-Disposition: form-data; name="username"\r\n\r\nshen&password=shentong\r\n------------------------------68c9ede00e93\r\nContent-Disposition: form-data; name="files"; filename="upload_file.txt"\r\nContent-Type: text/plain\r\n\r\nupload file test\n\r\n------------------------------68c9ede00e93--\r\n'
request.FILES=<MultiValueDict: {'files': [<InMemoryUploadedFile: upload_file.txt (text/plain)>]}>

可以看到,跟在 “?” 后的参数数据会保存到 request.GET 中,这也是 GET 请求带参数的方式。对于 POST 请求的传参,数据一般会保存在 request.POSTrequest.body 中。对于最后发送的上传文件请求,可以看到,文件内容的内容数据是保存到了 request.body 中。

3. 小结

本节内容我们主要介绍了如何向视图函数传送参数,包括两种方式:

  1. 通过动态的 URLconf 配置传递参数到视图函数中;

  2. 通过 http 请求带参数传递给视图函数。至此,Django 中的路由系统和视图函数已经介绍完毕。接下来会介绍 Django 中的模板系统。