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中操作列表之List.pop()方法的使用
May 21 Python
Python+selenium实现自动循环扔QQ邮箱漂流瓶
May 29 Python
django admin 后台实现三级联动的示例代码
Jun 22 Python
python生成带有表格的图片实例
Feb 03 Python
Python的matplotlib绘图如何修改背景颜色的实现
Jul 16 Python
利用Python产生加密表和解密表的实现方法
Oct 15 Python
python绘制雪景图
Dec 16 Python
Python3使用xlrd、xlwt处理Excel方法数据
Feb 28 Python
python接入支付宝的实例操作
Jul 20 Python
python 偷懒技巧——使用 keyboard 录制键盘事件
Sep 21 Python
python 30行代码实现蚂蚁森林自动偷能量
Feb 08 Python
Python中可变和不可变对象的深入讲解
Aug 02 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采集利器 Snoopy 试用心得
2011/07/03 PHP
PHP生成条形图的方法
2014/12/10 PHP
PHP实现的权重算法示例【可用于游戏根据权限来随机物品】
2019/02/15 PHP
在thinkphp5.0路径中实现去除index.php的方式
2019/10/16 PHP
一个不错的字符串转码解码函数(自写)
2014/07/31 Javascript
node.js使用require()函数加载模块
2014/11/26 Javascript
node.js中的path.normalize方法使用说明
2014/12/08 Javascript
jQuery获得包含margin的outerWidth和outerHeight的方法
2015/03/25 Javascript
jQuery实现的超酷苹果风格图标滑出菜单效果代码
2015/09/16 Javascript
基于JavaScript实现移动端点击图片查看大图点击大图隐藏
2015/11/04 Javascript
jquery无限级联下拉菜单简单实例演示
2015/11/23 Javascript
使用pcs api往免费的百度网盘上传下载文件的方法
2016/03/17 Javascript
require.js配合插件text.js实现最简单的单页应用程序
2016/07/12 Javascript
vue组件实例解析
2017/01/10 Javascript
JavaScript基础之this详解
2017/06/04 Javascript
vue刷新和tab切换实例
2018/02/11 Javascript
vue项目中应用ueditor自定义上传按钮功能
2018/04/27 Javascript
Vue无限滑动周选择日期的组件的示例代码
2018/07/18 Javascript
JS通过位运算实现权限加解密
2018/08/14 Javascript
JAVA面试题 static关键字详解
2019/07/16 Javascript
零基础写python爬虫之urllib2中的两个重要概念:Openers和Handlers
2014/11/05 Python
python 随机数使用方法,推导以及字符串,双色球小程序实例
2017/09/12 Python
PyQT实现菜单中的复制,全选和清空的功能的方法
2019/06/17 Python
python傅里叶变换FFT绘制频谱图
2019/07/19 Python
python3.8 微信发送服务器监控报警消息代码实现
2019/11/05 Python
Python+numpy实现矩阵的行列扩展方式
2019/11/29 Python
详解使用python3.7配置开发钉钉群自定义机器人(2020年新版攻略)
2020/04/01 Python
速卖通欧盟:Aliexpress EU
2020/08/19 全球购物
会计电算化应届生求职信
2013/11/03 职场文书
岗位职责范本
2013/11/23 职场文书
2014年安全生产目标责任书
2014/07/23 职场文书
联谊活动总结
2014/08/28 职场文书
2014年财务人员工作总结
2014/11/11 职场文书
2015年化验室工作总结
2015/04/23 职场文书
2016教师暑期培训学习心得体会
2016/01/09 职场文书
如何解决flex文本溢出问题小结
2022/07/15 HTML / CSS