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 相关文章推荐
Python中用pycurl监控http响应时间脚本分享
Feb 02 Python
python获取当前时间对应unix时间戳的方法
May 15 Python
Python编程实现两个文件夹里文件的对比功能示例【包含内容的对比】
Jun 20 Python
Python通过调用mysql存储过程实现更新数据功能示例
Apr 03 Python
python使用epoll实现服务端的方法
Oct 16 Python
Python3中的bytes和str类型详解
May 02 Python
把JSON数据格式转换为Python的类对象方法详解(两种方法)
Jun 04 Python
关于pytorch多GPU训练实例与性能对比分析
Aug 19 Python
100行Python代码实现每天不同时间段定时给女友发消息
Sep 27 Python
TensorFlow实现保存训练模型为pd文件并恢复
Feb 06 Python
python梯度下降算法的实现
Feb 24 Python
python实现FTP文件传输的方法(服务器端和客户端)
Mar 20 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 验证码不显示只有一个小红叉的解决方法
2013/09/30 PHP
Yii2中使用asset压缩js,css文件的方法
2016/11/24 PHP
PHP编程获取各个时间段具体时间的方法
2017/05/26 PHP
PHP isset()及empty()用法区别详解
2020/08/29 PHP
textarea的value是html文件源代码,存成html文件的代码
2007/04/20 Javascript
jQuery操作input type=radio的实现代码
2012/06/14 Javascript
Extjs 继承Ext.data.Store不起作用原因分析及解决
2013/04/15 Javascript
js导出格式化的excel 实例方法
2013/07/17 Javascript
JS随机生成不重复数据的实例方法
2013/07/17 Javascript
使用JavaScript和CSS实现文本隔行换色的方法
2015/11/04 Javascript
javascript正则表达式之分组概念与用法实例
2016/06/16 Javascript
bootstrapValidator表单验证插件学习
2016/12/30 Javascript
JavaScript实现前端分页控件
2017/04/19 Javascript
JavaScript实现一个带AI的井字棋游戏源码
2018/05/21 Javascript
js+html实现点名系统功能
2019/11/05 Javascript
JavaScript数组常用的增删改查与其他属性详解
2020/10/13 Javascript
Vue $attrs &amp; inheritAttr实现button禁用效果案例
2020/12/07 Vue.js
状态机的概念和在Python下使用状态机的教程
2015/04/11 Python
在Python上基于Markov链生成伪随机文本的教程
2015/04/17 Python
详解Python中time()方法的使用的教程
2015/05/22 Python
Python保存MongoDB上的文件到本地的方法
2016/03/16 Python
Python中easy_install 和 pip 的安装及使用
2017/06/05 Python
Python编程实现两个文件夹里文件的对比功能示例【包含内容的对比】
2017/06/20 Python
Python文件循环写入行时防止覆盖的解决方法
2018/11/09 Python
在Python 中实现图片加框和加字的方法
2019/01/26 Python
Python hexstring-list-str之间的转换方法
2019/06/12 Python
pyqt5 获取显示器的分辨率的方法
2019/06/18 Python
pytorch程序异常后删除占用的显存操作
2020/01/13 Python
Pandas中DataFrame交换列顺序的方法实现
2020/12/14 Python
CSS3 :nth-child()伪类选择器实现奇偶行显示不同样式
2013/11/05 HTML / CSS
巴西电子产品购物网站:Saldão da Informática
2018/01/09 全球购物
Java程序开发中如何应用线程
2016/03/03 面试题
中层干部竞争上岗演讲稿
2014/01/13 职场文书
母亲节感恩寄语
2014/02/21 职场文书
主题党日活动总结
2014/07/08 职场文书
学校食堂食品安全责任书
2014/07/28 职场文书