Python探索之URL Dispatcher实例详解


Posted in Python onOctober 28, 2017

URL dispatcher简单点理解就是根据URL,将请求分发到相应的方法中去处理,它是对URL和View的一个映射,它的实现其实也很简单,就是一个正则匹配的过程,事先定义好正则表达式和该正则表达式对应的view方法,如果请求的URL符合这个正则表达式,那么就分发这个请求到这个view方法中。

有了这个base,我们先抛出几个问题,提前思考一下:

这个映射定义在哪里?当映射很多时,如果有效的组织?

URL中的参数怎么获取,怎么传给view方法?

如何在view或者是template中反解出URL?

好,先来看一个简单的例子:

from django.conf.urls import patterns, url, include
urlpatterns = patterns('',
  url(r'^articles/2003/$', 'news.views.special_case_2003'),
  url(r'^articles/(\d{4})/$', 'news.views.year_archive'),
)
urlpatterns += patterns('',
  url(r'^articles/(?P<year>\d{4})/(?P<month>\d{2})/$', 'news.views.month_archive'),
  url(r'model/', include('model_test.urls')),
)

这段代码就是一个URL Dispatcher的例子,它在一个单独的python模块定义,Django中管这个模块叫做URLconf,其实,就是通过python代码方式实现的配置文件,在这个配置中定义了URL路径和对应的处理方法之间的映射。在Django中,是通过“树”的结构来管理URLconf之间的关系的,在Django中的主配置文件中,有一个叫做ROOT_URL_CONF的配置项,就是用来指定根URLconf,从根URLconf开始,逐条进行匹配,直到找到匹配项为止,这就是我们上面提到的第一个问题的答案,下面还会再仔细剖析。

在上面例子中,我们可以看到有3个方法:patterns, url, include。url方法构建了一个URL到View方法的映射关系对象,patterns将这些映射关系对象组织成为一个python的列表,那include是做什么的呢?它就是我们上面说到的“树”结构关系的联系者,include会关联其他的URLconf到本URLconf,也就是说include关联的是孩子节点。整个URL dispatcher体系,就是由这三个方法构建起来的,下面我们重点来介绍这三个方法,了解了这三个方法,整个URL映射机制就会非常清楚了。

def patterns(prefix, *args):
  pass
def url(regex, view, kwargs=None, name=None, prefix=''):
  pass
def include(arg, namespace=None, app_name=None):
  pass

url()

先来看下最重要的url()方法。第一个参数regex是代表URL的正则表达式,第二个参数指定了和该正则表达式映射的View,此外,还可以通过kwargs参数,给view方法指定默认的kwargs参数,还有name参数,用来命名该URL,主要用在URL反解中,至于prefix用处不大,不解释。

url()方法最终构造了一个对象,我们姑且叫它URL映射对象,当第一次访问这个对象去匹配URL时,它会把这个对象中的正则表达式编译一次,然后保存在该对象中,所以以后再次匹配时,就会很快,不会重复编译该正则表达式了。在这里正则匹配其实就是用就是python的re模块,使用过程大致如下:

# 第一次访问时,编译,然后保存在url对象中
regex = re.compile(regex_str, re.UNICODE)
# 每次URL访问时,进行正则匹配
match = regex.search(path)
kwargs = match.groupdict()
args = match.groups()

注意,这里涉及到了上面提到的第二个问题,即URL中的参数是如何获取,如何传递给view方法的。从URL中获取参数,其实是通过re模块中named groups和non-named groups的概念来获取的,通过match.groupdict()得到的是named groups,其实就是一个字典,字典的key是在URL中指定的,该字典会作为kwargs参数传递给view,而通过match.groups()得到的是non-named groups,是一个元组,即tuple,该元组会作为args参数传递给view。不过,这里的args和kwargs是不能够同时存在的,当有kwargs不为空时,args就会被置空,当kwargs为空时,args才会被用到,而传递给view的kwargs就只有url()方法中指定的默认kwargs。也就是说,如果你在URL中使用了named groups,那么non-named groups就会被忽略,如果只使用了non-named groups,它才会被作为args参数,传递给view方法。

好,纠结了一大堆,还是来解析一下我们上面提到的例子,假如我们有url和view

urls:

url(r'^articles/(\d{4})/$', 'news.views.year_archive')
url(r'^articles/(?P<year>\d{4})/(?P<month>\d{2})/$', 'news.views.month_archive')

views:

def year_archive(request, *args, **kwargs):
  pass
def month_archive(request, *args, **kwargs):
  pass

当我们访问”articles/2014/”这个路径的时候,解析的过程如下:

>>> import re
>>> regex = re.compile(r'^articles/(\d{4})/$', re.UNICODE)
>>> match = regex.search("articles/2014/")
>>> match.groupdict()
{}
>>> match.groups()
('2014',)

所以最终传递给year_archive()方法中的参数应该是这样的:

(Pdb) pp args
(u'2014',)
(Pdb) pp kwargs
{}

当我们访问”articles/2014/11”这个路径时,解析的过程如下:

>>> import re
>>> regex = re.compile(r'^articles/(?P<year>\d{4})/(?P<month>\d{2})/$', re.UNICODE)
>>> match = regex.search("articles/2014/11/")
>>> match.groupdict()
{'year': '2014', 'month': '11'}
>>> match.groups()
('2014', '11')

所以最终传递给month_archive()方法中的参数应该是这样的:

(Pdb) pp args
()
(Pdb) pp kwargs
{'month': u'11', 'year': u'2014'}

再罗嗦一句,因为url()可以指定一个kwargs参数,它是该url关联的view()方法的默认kwargs参数,也就是说如果在url()方法中指定了kwargs,那么会将这个参数的内容,也传递到view方法中的kwargs参数中。

好,至此,url()方法基本上就清楚了,第二个问题也解决了,至于name参数,到下面讲到URL反解的时候再详细解释。

patterns()

接下来,我们来看patterns()方法,这个其实比较简单,它就是返回一个由url()方法构造的URL映射对象组成的列表。它有一个必填参数是prefix,这个prefix是它所包含的view的公共前缀,这么做是为了避免代码重复,比如:

urlpatterns = patterns('',
  url(r'^articles/(\d{4})/$', 'news.views.year_archive'),
  url(r'^articles/(\d{4})/(\d{2})/$', 'news.views.month_archive'),
  url(r'^articles/(\d{4})/(\d{2})/(\d+)/$', 'news.views.article_detail'),
)

可以写成:

urlpatterns = patterns('news.views',
  url(r'^articles/(\d{4})/$', 'year_archive'),
  url(r'^articles/(\d{4})/(\d{2})/$', 'month_archive'),
  url(r'^articles/(\d{4})/(\d{2})/(\d+)/$', 'article_detail'),
)

注意,由patterns()生成的列表,被赋值给urlpatterns这个变量,这个变量是不能随便定义的,必须是约定好的,默认django会去URLconf中查找这个变量,也许你可以在某个地方设定一个参数,来换个约定,改变一下这个变量名。

在2.0版本的Django中,会舍弃这个方法,而是直接赋值给urlpatterns一个列表,不做过多讨论。

include()

我们来说include(),这其实是个难点,关键在于URL反解那里,Django的文档也没有说清楚,而且关系也比较乱,所以,必须得实际的测试一下,才会明白。

上面说过,include()是“树”结构关系的联系者,include会关联其他的URLconf到本URLconf,靠include()才能够让Django的URL设计变得非常的灵活和简洁。include()有三个参数,第一个参数不必多说,它指定了要包含的其它URLconf的路径,关键是剩下的两个参数,一个是namespace, 一个是app_name,有什么用呢?其实,这两个参数再加上url()方法中的name参数,共同构成了Django中URL的命名空间,而命名空间主要是为了URL反解的,那什么是URL反解呢?我们现在能根据请求的一个URL路径,找到对应的view处理方法,那么反过来,我们在view方法中,或者是template中,根据传递过来的参数,能够解析出对应的URL,这就是URL反解。为什么需要URL反解呢?主要是为了不要把程序写死了,如果我们在html中直接把路径写死了,那么以后改起来就会非常的麻烦,所以常常会把这些可变的东西放到一个变量中,在程序中引用的是这个变量名,这是写程序的一个常识吧。所以,我们能从这个“树”中,从上到下,也得能够从下到上。

在template中进行反解使用的是{%url%}这个tag,在view中,进行反解,使用的是`django.core.urlresolvers.reverse()这个方法。

好,先来看一个最简单的例子:

mydjango/urls.py:

urlpatterns = patterns('',
  url(r'model/', include('model_test.urls')),
)

model_test/urls.py:

urlpatterns = patterns('',
  url(r'^$', views.index, name='index'),
)

mydjango/urls.py是根URLconf,它包含了model_test的URLconf,modul_test中的urlpatterns中有一个命名为index的url映射对象。

如果我们想在template中得到这个view对应的url的真实路径,那么用template的url tag就行了:

{% url 'index' %}

这样得到的结果就是: /model/。

同理,如果在view方法中,那么使用reverve()方法:

from django.core.urlresolvers import reverse
reverse("index")

得到的也是: /model/

在这个例子中,我们只使用到了url()方法中的name参数,并没有用到命名空间,因为这种简单的情况,没有产生混淆,还没有必要用到命名空间,使用命名空间的主要有以下两种情况:

当在一个项目中,有多个应用,应用中定义的url映射对象的name有可能有重复的,这样当进行反解时,Django就不能确定到底是哪个应用了

当在一个项目中,同一个应用,被部署多个实例时,这多个实例之间是共享定义的name url的,所以在进行反解时,也不能确定,到底是哪个实例

第一种情况,其实是比较好解决的,在每一个应用的include()中,指定不同的namespace参数就可以了,如:

mydjango/urls.py:

urlpatterns = patterns('',
  url(r'model/', include('model_test.urls', namespace='model')),
)

这样,在template中或者是reverse中,在name前需要加上namepace进行反解:

{% 'model:index' %}
or 
reverse("model:index")

这样就可以准确的反解到model_test这个应用中。

第二种情况,什么叫“一个应用,被部署多个实例”呢?其实就是这种情况:

urlpatterns = patterns('',
  url(r'model1/', include('model_test.urls')),
  url(r'model2/', include('model_test.urls')),
)

不同的路径下,引用的是相同的应用,同一个应用,被实例化了两次,这种情况,怎么进行区分呢?我能像第一种情况一样,在include中指定不同的namespace来解决问题吗?答案是可行的,但是不推荐。要知道,他们引用的是同一个应用,同一个应用意味着什么,意味着代码是一样的,你在同一份代码中,通过if/else来判断该反解到哪个namespace中,这个做法是非常不优雅的,严重违背了Django的DRY原则。

那Django通过什么办法来解决这个问题呢?它通过app_name + namespace + current_app的方式来解决。namespace, app_name分别为include()的第二个和第三个参数,app_name指定这个应用的名称,namespace指定这个应用某个实例的url的命名空间,current_app则是根据请求的路径,解析出的该url的命名空间,也就是namespace,在进行反解时,动态的将该current_app传递给反解的函数中,反解的函数就可以根据这个namespace,来确定应该反解到哪个实例中了。同一个应用的多个实例的app_name应该是相同的,而namespace应该是不同的。可能有点乱了,我们再来举个例子:

mydjango/urls.py:

urlpatterns = patterns('',
  url(r'model1/', include('model_test.urls', namespace='model_1', app_name="app")),
  url(r'model2/', include('model_test.urls', namespace='model_2', app_name="app")),
)

model_test/urls.py:

urlpatterns = patterns('',
  url(r'^$', views.index, name='index'),
)

model_test/views.py:

from django.shortcuts import render
from django.core.urlresolvers import reverse

def index(request):
  current_app = request.resolver_match.namespace
  print reverse("app:index", current_app=current_app)
  return render(request, 'model/index.html', current_app=current_app)

model_test/templates/model/index.html:

{% url 'app:index' %}

在view中,首先获得当前的namespace,然后通过current_app传递给reverse(),reverse就可以知道应该解析到哪个实例中了。同理,在templace中,也需要将current_app传递过去。这样,我们就可以动态的反解URL了:

当我们请求的路径是/model1/时,current_app就是model_1,再根据app_name,就可以准确的反解出该URL为:/model1/,
如果请求的路径是/model2/,那么current_app就是model_2,反解的路径就是/model2/。

虽然有点复杂,但是这种情况用的比较少,google了很久,才把这种情况大概弄清楚,也许理解的有不对的地方,待以后实践去检验,现在关键在于理解这种机制思想。

全文完,更多详细的内容,参见Django官方文档:https://docs.djangoproject.com/en/1.6/topics/http/urls/

总结

以上就是本文关于Python探索之URL Dispatcher实例详解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续啊参阅本站:Python探索之Metaclass初步了解、Python编程之Re模块下的函数介绍等,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持。

Python 相关文章推荐
go和python调用其它程序并得到程序输出
Feb 10 Python
Python中MySQL数据迁移到MongoDB脚本的方法
Apr 28 Python
深入理解Python中的内置常量
May 20 Python
基于Python和Scikit-Learn的机器学习探索
Oct 16 Python
理解python中生成器用法
Dec 20 Python
win7下python3.6安装配置方法图文教程
Jul 31 Python
解决Python下json.loads()中文字符出错的问题
Dec 19 Python
Pycharm 使用 Pipenv 新建的虚拟环境(图文详解)
Apr 16 Python
Python CategoricalDtype自定义排序实现原理解析
Sep 11 Python
OpenCV读取与写入图片的实现
Oct 13 Python
利用Python网络爬虫爬取各大音乐评论的代码
Apr 13 Python
python数字图像处理数据类型及颜色空间转换
Jun 28 Python
Python探索之Metaclass初步了解
Oct 28 #Python
Python编程之Re模块下的函数介绍
Oct 28 #Python
Python探索之静态方法和类方法的区别详解
Oct 27 #Python
Python探索之爬取电商售卖信息代码示例
Oct 27 #Python
Python 列表理解及使用方法
Oct 27 #Python
Python算法之求n个节点不同二叉树个数
Oct 27 #Python
Python探索之自定义实现线程池
Oct 27 #Python
You might like
详细介绍PHP应用提速面面观
2006/10/09 PHP
Dwz与thinkphp整合下的数据导出到Excel实例
2014/12/04 PHP
PHP编写daemon process详解及实例代码
2016/09/30 PHP
php实现构建排除当前元素的乘积数组方法
2018/10/06 PHP
在laravel框架中使用model层的方法
2019/10/08 PHP
javascript 的Document属性和方法集合
2010/01/25 Javascript
jqGrid jQuery 表格插件测试代码
2011/08/23 Javascript
js实现杯子倒水问题自动求解程序
2013/03/25 Javascript
简单js代码实现selece二级联动(推荐)
2014/02/18 Javascript
Zero Clipboard实现浏览器复制到剪贴板的方法(多个复制按钮)
2016/03/24 Javascript
浅谈如何实现easyui的datebox格式化
2016/06/12 Javascript
Angular 页面跳转时传参问题
2016/08/01 Javascript
jQuery Ajax 异步加载显示等待效果代码分享
2016/08/01 Javascript
关于vue.js v-bind 的一些理解和思考
2017/06/06 Javascript
Vue.js实现一个todo-list的上移下移删除功能
2017/06/26 Javascript
mac中利用NVM管理不同node版本的方法详解
2017/11/08 Javascript
js登录滑动验证的实现(不滑动无法登陆)
2018/01/03 Javascript
Vue实现点击后文字变色切换方法
2018/02/11 Javascript
vue-cli项目代理proxyTable配置exclude的方法
2018/09/20 Javascript
JavaScript常用工具函数库汇总
2020/09/17 Javascript
python实现探测socket和web服务示例
2014/03/28 Python
使用Python的package机制如何简化utils包设计详解
2017/12/11 Python
在Python dataframe中出生日期转化为年龄的实现方法
2018/10/20 Python
对pandas里的loc并列条件索引的实例讲解
2018/11/15 Python
Python发送邮件测试报告操作实例详解
2018/12/08 Python
使用k8s部署Django项目的方法步骤
2019/01/14 Python
python选取特定列 pandas iloc,loc,icol的使用详解(列切片及行切片)
2019/08/06 Python
Python使用itchat模块实现群聊转发,自动回复功能示例
2019/08/26 Python
浅谈Django前端后端值传递问题
2020/07/15 Python
css3实现3D色子翻转特效
2014/12/23 HTML / CSS
最新党员的自我评价分享
2013/11/04 职场文书
挑战杯创业计划书的写作指南
2014/01/07 职场文书
竞聘副主任科员演讲稿
2014/01/11 职场文书
上级检查欢迎词
2014/01/18 职场文书
学生违纪检讨书200字
2014/10/21 职场文书
微信小程序APP的事件绑定以及传递参数时的冒泡和捕获
2022/04/19 Javascript