Django源码剖析(01)--url匹配

对Django-url的源码进行小小的解析

Define: Django版本为1.9.8

1. 基本定义

settings.url中, 我们可以定义如下url:

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.conf.urls import url

order_urls = [
url(r"^order/(?P<pk>[0-9+])/$", OrderView.as_view()),
url(r"^order/", CreateOrderView.as_view())
]

urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r"order/", include(order_urls)), # 使用include
url(r"cache", test_cache), # 使用视图函数
url(r"get_product", GetProductAPIView.as_view()) # 使用视图类, 因为是测试
#就不用RESTful-API来书写了.

2. 源码分析

直接从django.conf.urls.url函数入手, 可以来看一下:

1
2
3
4
5
6
7
8
9
10
def url(regex, view, kwargs=None, name=None):
if isinstance(view, (list, tuple)):
# For include(...) processing.
urlconf_module, app_name, namespace = view
return RegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace)
elif callable(view):
# 在该测试用例下, kwargs, name均为None
return RegexURLPattern(regex, view, kwargs, name)
else:
raise TypeError('view must be a callable or a list/tuple in the case of include().')

函数看起来比较简单, 第一层if对应include行为, 第二层对应常规的视图函数或者视图类, 再后面就是异常的处理了.
可以看到返回的对象是不同的, 一个是RegexURLResolver, 另一个却是RegexURLPattern, 可以看做是一个简单工厂模式, 根据传入的参数不同, 返回不同的对象.
那么先看哪个?当然是简单的那个了, 因为在绝大多数情况下, 复杂情况只是在简单情况下进行的一次或者多次封装而已.所以直接怼到RegexURLPattern源代码中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class RegexURLPattern(LocaleRegexProvider):
def __init__(self, regex, callback, default_args=None, name=None):
LocaleRegexProvider.__init__(self, regex) # 调用父类__init__方法, 为什么不用super这里我也不知道.
self.callback = callback # the view 这里很贴心的给出了注释, callback就是视图函数
self.default_args = default_args or {} # self.default_args = {}
self.name = name # self.name = None

def __repr__(self):
# 字符串表示, 没有什么值得关注的
return force_str('<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern))

def check(self):
warnings = self._check_pattern_name()
if not warnings:
# 如果self.name没有冒号的话, 检查URL是否以`/`开头, 这里调用的父类的方法, 没有什么可看的, 直接贴一个官方注释
# Check that the pattern does not begin with a forward slash.
warnings = self._check_pattern_startswith_slash()
return warnings

def _check_pattern_name(self):
"""
Check that the pattern name does not contain a colon.
检查self.name中是否包含冒号, 在这里并不能知道为什么self.name中不能包含冒号
"""
if self.name is not None and ":" in self.name:
warning = Warning(
"Your URL pattern {} has a name including a ':'. Remove the colon, to "
"avoid ambiguous namespace references.".format(self.describe()),
id="urls.W003",
)
return [warning]
else:
return []

def resolve(self, path):
match = self.regex.search(path)
if match:
# If there are any named groups, use those as kwargs, ignoring
# non-named groups. Otherwise, pass all non-named arguments as
# positional arguments.
kwargs = match.groupdict()
args = () if kwargs else match.groups()
# In both cases, pass any extra_kwargs as **kwargs.
kwargs.update(self.default_args)
return ResolverMatch(self.callback, args, kwargs, self.name)

到这里其实也就足够了, 不需要太过于关注__init__之外的函数用处, 当然一眼就能看出作用的函数看一下也是可以的.这样一来, url函数在参数为视图函数时其实的是一个RegexURLPattern对象.
再来看RegexURLResolver, 在此之前需要过一下include函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 这里剔除了一些不必要的warning, 以简化代码
def include(arg, namespace=None, app_name=None):
# 在该测试用例下, arg == order_urls, 是一个list
if app_name and not namespace:
raise ValueError('Must specify a namespace if specifying app_name.')
if app_name:
warnings.warn("")

if isinstance(arg, tuple):
# 测试用例为list, 故不会进入该控制分支中, 所以干掉下面的代码
...
else:
# No namespace hint - use manually provided namespace
urlconf_module = arg # urlconf_module == order_urls

if isinstance(urlconf_module, six.string_types):
urlconf_module = import_module(urlconf_module)
# urlconf_module该list并没有urlpatterns这个属性, 故patterns == urlconf_module
patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
app_name = getattr(urlconf_module, 'app_name', app_name) # app_name = None
if namespace and not app_name:
warnings.warn("")

namespace = namespace or app_name

if isinstance(patterns, (list, tuple)):
for url_pattern in patterns:
if isinstance(url_pattern, LocaleRegexURLResolver):
raise ImproperlyConfigured('Using i18n_patterns in an included URLconf is not allowed.')

return (urlconf_module, app_name, namespace)

由于order_urls是由url()函数所组成list, 而url()函数在上面我们得知将会返回RegexURLPattern对象, 那么urlconf_module其实就是由RegexURLPattern所组成的列表.
在该测试用例中, 将会返回(RegexURLPattern-list, None, None)这样的一个元组.

现在我们终于可以来审查RegexURLResolver的源代码了.

  • 首先是__init__:
1
2
3
4
5
6
def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
LocaleRegexProvider.__init__(self, regex) # 调用父类__init__方法
self.urlconf_name = urlconf_name # 这里的urlconf_name就是上述中的urlconf_module, 是个RegexURLPattern-list
self.callback = None
... # 其余参数均为初始化, 不需要太关心.
self._local = threading.local()
  • check以及该方法所调用的私有方法, 主要是对url进行一些检查, 比较重要的是self.url_patterns, 类作为装饰器进行装饰, 还是比较少见的.万变不离其宗, @这个语法糖的作用就是:
1
url_patterns = cached_property(url_patterns)

所以最终url_patterns就是一个cached_property对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cached_property
def url_patterns(self):
# urlconf_module might be a valid set of patterns, so we default to it
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
try:
iter(patterns)
except TypeError:
msg = ("")
raise ImproperlyConfigured(msg.format(name=self.urlconf_name))
return patterns

class cached_property(object):
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
"""
def __init__(self, func, name=None):
self.func = func
self.__doc__ = getattr(func, '__doc__')
self.name = name or func.__name__

def __get__(self, instance, cls=None):
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res

可以看到cached_property对象保留了原函数的一些信息, 包括函数注释以及函数名称.这里的__get__方法起到了比较关键的作用.
object.__get__(self, instance, cls), 如果一个class定义了__get__方法,则这个class就可以称为descriptor(这个说法不是很准确, 大致可以这样认为)。cls是所有者的类,instance是访问descriptor的实例,如果不是通过实例访问,而是通过类访问的话,instance则为None。
一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person(object):
def __init__(self, name):
self.name = name

def __get__(self, instance, cls):
print("__get__ method called")
return self

class NewPerson():
person = Person()

if __name__ == "__main__":
new_person = NewPerson()
new_person.person

out:
# __get__ method called
# <__main__.Person object at 0x7fd37b852320>

在上实例中, instance为<__main__.NewPerson object at 0x7fb3da55e358>, cls为<class '__main__.NewPerson'>

再回到RegexURLResolver的代码中, 可以和奇怪的发现, 根本找不到self.url_patterns是在哪个地方初始化的.其实这里的self就是instance, cls自然也就是RegexURLResolver, 那么最终的效果就是:

1
self.url_patterns = self.url_patterns()

self.url_patterns中与self.urlconf_module中内容相同.里面是啥就不再重复了.
到这里其实也就差不多了, 整理一下, 只需要知道我们自己定义的urlpatterns这里list里面到底是个啥就行.

1
2
3
4
5
[
<RegexURLResolver <RegexURLPattern list> (admin:admin) ^admin/>,
<RegexURLResolver <RegexURLPattern list> (None:None) order/>,
<RegexURLPattern None cache>
]

接下来, 就是最重要的方法, resolve方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def resolve(self, path):
path = force_text(path) # path may be a reverse_lazy object
tried = []
match = self.regex.search(path)
if match:
new_path = path[match.end():] # 假如说请求路径为`localhost:8000/cache`, 那么new_path == "cache"
for pattern in self.url_patterns: # self.url_patterns就是上面的list
try:
# 递归调用
sub_match = pattern.resolve(new_path)
except Resolver404 as e:
sub_tried = e.args[0].get('tried')
if sub_tried is not None:
tried.extend([pattern] + t for t in sub_tried)
else:
tried.append([pattern])
else:
if sub_match:
# Merge captured arguments in match with submatch
sub_match_dict = dict(match.groupdict(), **self.default_kwargs)
sub_match_dict.update(sub_match.kwargs)

# If there are *any* named groups, ignore all non-named groups.
# Otherwise, pass all non-named arguments as positional arguments.
sub_match_args = sub_match.args
if not sub_match_dict:
sub_match_args = match.groups() + sub_match.args

return ResolverMatch(
sub_match.func,
sub_match_args,
sub_match_dict,
sub_match.url_name,
[self.app_name] + sub_match.app_names,
[self.namespace] + sub_match.namespaces,
)
tried.append([pattern])
raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path': path})

最终呢, 在调用方可以得到ResolverMatch对象, 里面的东西其实少的可怜:
Alt text

我还是贴代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# django.core.handlers.base.py

resolver_match = resolver.resolve(request.path_info) # 得到了url匹配
callback, callback_args, callback_kwargs = resolver_match # 做一个拆包
request.resolver_match = resolver_match # request对象添加属性

# Apply view middleware # 该逻辑分支暂时不用管
for middleware_method in self._view_middleware:
response = middleware_method(request, callback, callback_args, callback_kwargs)
if response:
break

if response is None:
# 这里是对非事务处理的view进行一些操作, 很少会用到
wrapped_callback = self.make_view_atomic(callback)
try:
# 就是在这里, 真正的调用了我们的视图函数, 并返回response对象
response = wrapped_callback(request, *callback_args, **callback_kwargs)
except Exception as e:
response = self.process_exception_by_middleware(e, request)

3. 总结

url匹配的问题基本上到这里也就基本结束了, 余下的与request, response有关, 并不在url分析范畴之内, 后续再进行讨论.总得来看, 代码数量还是很多的, 但是真正的核心代码只有那么一些, 其余都是为核心代码所做的各种判断服务, 以便于核心代码正常运行.
看懂代码仅仅是第一层, 我们需要做到的是学以致用, 在我们自己的日常开发中应用这些优秀的代码思想.

4. 需要继续深入了解的特性

  • 类作为装饰器并运用属性描述符
  • 递归的深度优先遍历匹配url, 能否使用B-Tree进行优化?