bigywater.tech
Django · 16

reverse() 如何生成 URL

reverse() 是把视图名变回 URL 的那半边。它有哪些行为你以为知道、其实没验证过:参数类型怎么校验、同名怎么选、命名空间怎么解析、第一次调用有多贵?这篇逐条实测。

校验
反向时参数也要能匹配模式的正则:int 路径传 "abc" 会 NoReverseMatch
后定义的胜出
两条同名同参数的模式,reverse 返回后定义的那条
140 ms → 7.8 µs
N=5000 个命名路由时 reverse 第一次 vs 之后每次的耗时

参数与转换器

实测 · 本机执行输出
reverse('post', args=[5])                                        → /post/5/
reverse('post', kwargs={'pk': 5})                                → /post/5/
reverse('post', args=['5'])                                      → /post/5/
reverse('post', args=['abc'])                                    → NoReverseMatch: Reverse for 'post' with arguments '('abc',)' not found. 1 pattern(s) tried: ['post/(?P<pk>[0-9]+)/\\Z']
reverse('post')                                                  → NoReverseMatch: Reverse for 'post' with no arguments not found. 1 pattern(s) tried: ['post/(?P<pk>[0-9]+)/\\Z']
reverse('post', args=[5, 6])                                     → NoReverseMatch: Reverse for 'post' with arguments '(5, 6)' not found. 1 pattern(s) tried: ['post/(?P<pk>[0-9]+)/\\Z']
reverse('post', kwargs={'pk': 5, 'x': 1})                        → NoReverseMatch: Reverse for 'post' with keyword arguments '{'pk': 5, 'x': 1}' not found. 1 pattern(s) tried: ['post/(?P<pk>[0-9]+)/\\Z']
reverse('post-edit', kwargs={'slug': 'a b/é'})                   → NoReverseMatch: Reverse for 'post-edit' with keyword arguments '{'slug': 'a b/é'}' not found. 1 pattern(s) tried: ['post/(?P<slug>[-a-zA-Z0-9_]+)/edit/\\Z']
reverse('post-edit', kwargs={'slug': 'ok-slug_1'})               → /post/ok-slug_1/edit/
reverse('arch', kwargs={'year': 7})                              → /arch/0007/
reverse('arch', kwargs={'year': 2026})                           → /arch/2026/
reverse('files', kwargs={'p': 'a b/c d.txt'})                    → /files/a%20b/c%20d.txt
reverse('re', args=['1', '2'])                                   → /re/1/2/
reverse('re', kwargs={'a': 1, 'b': 2})                           → /re/1/2/
reverse('nope')                                                  → NoReverseMatch: Reverse for 'nope' not found. 'nope' is not a valid view function or pattern name.

报错信息里有「已尝试的模式」和你传入的参数,基本足够定位。注意 args=['5'](字符串)也能反向出 /post/5/——因为反向时是用模式的正则去匹配参数的字符串形式。

同名与命名空间

实测 · 本机执行输出
opt(无参数)        → /opt/
opt(page=3)        → /opt/3/
dup(两个同名同参数) → /dup/two/   (定义顺序: dup/one/ 在前, dup/two/ 在后)
v1:users → /api/users/
v2:user  → /api2/users/3/
reverse('api:users', current_app='v2') → /api2/users/

同一个 name 出现多次是允许的:Django 会按你传入的参数找能匹配的那条(opt 的两个模式)。如果两条都能匹配,后定义的胜出——所以给不同页面起同一个 name 是个静悄悄的 bug。

代价

实测 · 本机执行输出
  N=10    第一次 reverse     1.08 ms   之后每次   7.56 µs
  N=1000  第一次 reverse    27.08 ms   之后每次   7.64 µs
  N=5000  第一次 reverse   139.83 ms   之后每次   7.75 µs

第一次 reverse() 会构建整个 urlconf 的反向字典(与路由数量成正比),之后每次调用几乎是常数。这也是 Django 进程「第一个请求慢一点」的来源之一。

分步演示:reverse 的规则

reverse()
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。耗时为进程内调用,每种规模取 5 轮里最快的一轮
    • 没有验证:reverse_lazy、i18n 前缀下的 reverse、include() 深层嵌套与 namespace 冲突、reverse 结果与反向 resolve 的往返一致性(除本文几个例子)
    京ICP备2026067502号-1京公网安备11010502063487号