Django 源碼小剖: URL 調度器(URL dispatcher)

在剛開始接觸 django 的時候, 咱們嘗試着從各類入門文檔中建立一個本身的 django 項目, 須要在 mysite.urls.py 中配置 URL. 這是 django url 匹配處理機制的一小部分.python

URL 調度器詳解

django url 匹配處理機制主要由一下模塊實現: django.conf.urls 和 django.core.urlresolver.py. 有須要摘取上一節中的代碼:git

# BaseHandler.get_response() 的定義
# 處理請求的函數, 並返回 response
def get_response(self, request):
    "Returns an HttpResponse object for the given HttpRequest"
    根據請求, 獲得響應

    try:
        爲該線程提供默認的 url 處理器
        # Setup default url resolver for this thread, this code is outside
        # the try/except so we don't get a spurious "unbound local
        # variable" exception in the event an exception is raised before
        # resolver is set

        #ROOT_URLCONF = 'mysite.urls'
        urlconf = settings.ROOT_URLCONF

        # set_urlconf() 會設置 url 配置即 settings.ROOT_URLCONF
        # 會設置一個線程共享變量, 存儲 urlconf
        urlresolvers.set_urlconf(urlconf)

        # 實例化 RegexURLResolver, 暫且將其理解爲一個 url 的匹配處理器, 下節展開
        resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)

        try:
            response = None

            # Apply request middleware 調用請求中間件
            ......

            # 若是沒有結果
            if response is None:
                # 嘗試 request 中是否有 urlconf, 通常沒有, 能夠忽略此段代碼!!!
                if hasattr(request, 'urlconf'):
                    # Reset url resolver with a custom urlconf. 自定義的 urlconf
                    urlconf = request.urlconf
                    urlresolvers.set_urlconf(urlconf)
                    resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)
                # 調用 RegexURLResolver.resolve(), 能夠理解爲啓動匹配的函數; 返回 ResolverMatch 實例
                resolver_match = resolver.resolve(request.path_info)

                # resolver_match 對象中存儲了有用的信息, 譬如 callback 就是咱們在 views.py 中定義的函數.
                callback, callback_args, callback_kwargs = resolver_match

                # 將返回的 resolver_match 掛鉤到 request
                request.resolver_match = resolver_match

                # Apply view middleware 調用視圖中間件
                ......

能夠簡單的理解爲 get_response() 中構造了 RegexURLResolver 對象並調用了 RegexURLResolver.resolve(path) 啓動解析. 從上面的代碼中, 能夠獲知 urlconf 默認使用的是 mysite.settings.py 中的 ROOT_URLCONF, 而也確實能夠在 mysite.settings.py 中找到對應的設置項, 而且作出修改.github

從上, 至少能夠知道, 真正發揮匹配做用的是 RegexURLResolver 對象, 並調用 RegexURLResolver.resolve() 啓動了解析, 一切從這裏開始. 從 urlresolver.py 中抽取主幹部分, 能夠獲得下面的 UML 圖:正則表達式

url_dispatcher_uml

LocaleRegexProvider 類只爲地區化而存在, 他持有 regex 屬性, 但在 RegexURLResolver 和 RegexURLPattern 中發揮不一樣的做用:django

  • RegexURLResolver: 過濾 url 的前綴, 譬如若是 regex 屬性值爲 people, 那麼能將 people/daoluan/ 過濾爲 daoluan/.
  • RegexURLPattern: 匹配整個 url.

在展開 ResolverMatch, RegexURLPattern, RegexURLResolver 三個類以前, 暫且將他們理解爲:app

  • ResolverMatch 當匹配成功時會實例化返回
  • RegexURLPattern, RegexURLResolver 匹配器, 但有不一樣.

而後須要先介紹三個函數: url(), include(), patterns(), 三者常常在 urls.py 中用到. 它們在 django.conf.urls 中定義. 摘抄和解析以下:ide

# url 裏面能夠用 incude 函數
def include(arg, namespace=None, app_name=None):
    if isinstance(arg, tuple):
        # callable returning a namespace hint
        if namespace:
            raise ImproperlyConfigured('Cannot override the namespace for a dynamic module that provides a namespace')

        # 獲取 urlconf 模塊文件, 應用名, 命名空間
        urlconf_module, app_name, namespace = arg
    else:
        # No namespace hint - use manually provided namespace
        urlconf_module = arg

    if isinstance(urlconf_module, six.string_types):
        # 嘗試導入模塊
        urlconf_module = import_module(urlconf_module)

    # 在 urlconf_module 中導入 urlpatterns
    # 在 urlconf_module 中確定會有 urlpatterns 這個變量
    patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)

    # Make sure we can iterate through the patterns (without this, some
    # testcases will break).
    if isinstance(patterns, (list, tuple)):
        for url_pattern in patterns:
            # Test if the LocaleRegexURLResolver is used within the include;
            # this should throw an error since this is not allowed!
            if isinstance(url_pattern, LocaleRegexURLResolver):
                raise ImproperlyConfigured(
                    'Using i18n_patterns in an included URLconf is not allowed.')

    # 返回模塊, app 名 ,命名空間
    return (urlconf_module, app_name, namespace)

def patterns(prefix, *args): 特地留一個 prefix
    pattern_list = []
    for t in args:
        if isinstance(t, (list, tuple)):
            t = url(prefix=prefix, *t) 自動轉換

        elif isinstance(t, RegexURLPattern):
            t.add_prefix(prefix)

        pattern_list.append(t)

    # 返回 RegexURLResolver 或者 RegexURLPattern 對象的列表
    return pattern_list

# url 函數
def url(regex, view, kwargs=None, name=None, prefix=''):
    if isinstance(view, (list,tuple)): 若是是 list 或者 tuple
        # For include(...) processing. 處理包含 include(...)
        urlconf_module, app_name, namespace = view

        # 此處返回 RegexURLResolver, 區分下面返回 RegexURLPattern
        return RegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace)
    else:
        if isinstance(view, six.string_types):
            if not view:
                raise ImproperlyConfigured('Empty URL pattern view name not permitted (for pattern %r)' % regex)
            if prefix:
                view = prefix + '.' + view

        # 返回 RegexURLPattern 的對象
        return RegexURLPattern(regex, view, kwargs, name)
    # 從上面能夠獲知, url 會返回 RegexURLResolver 或者 RegexURLPattern 對象

能夠簡單的理解爲, url() 根據具體狀況返回 RegexURLResolver 或者 RegexURLPattern 對象; patterns() 返回了包含有 RegexURLPattern 和 RegexURLResolver 對象的列表. 當在 urls.py 中出現:
每一個 include() 的時候, 最終會產生一個 RegexURLResolver 對象;
不然爲 RegexURLPattern 對象.函數

回到那三個類, 摘取 RegexURLResolver 的主幹函數做爲講解:
# 最關鍵的函數
def resolve(self, path):

    tried = []

    # regex 在 RegexURLResolver 中表示前綴
    match = self.regex.search(path)

    if match:
        # 去除前綴
        new_path = path[match.end():]

        for pattern in self.url_patterns: # 窮舉全部的 url pattern
            # pattern 是 RegexURLPattern 實例
            try:

"""在 RegexURLResolver.resolve() 中的一句: sub_match = pattern.resolve(new_path) 最爲關鍵.
從上面 patterns() 函數的做用知道, pattern 能夠是 RegexURLPattern 對象或者 RegexURLResolver 對象. 當爲 RegexURLResolver 對象的時候, 就是啓動子 url 匹配處理器, 因而又回到了上面.

RegexURLPattern 和 RegexURLResolver 都有一個 resolve() 函數, 因此, 下面的一句 resolve() 調用, 能夠是調用 RegexURLPattern.resolve() 或者 RegexURLResolver.resolve()"""

                # 返回 ResolverMatch 實例
                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:
                    # match.groupdict()
                    # Return a dictionary containing all the named subgroups of the match,
                    # keyed by the subgroup name.

                    # 若是在 urls.py 的正則表達式中使用了變量, match.groupdict() 返回即爲變量和值.
                    sub_match_dict = dict(match.groupdict(), **self.default_kwargs)

                    sub_match_dict.update(sub_match.kwargs)

                    # 返回 ResolverMatch 對象, 如你所知, 獲得此對象將能夠執行真正的邏輯操做, 即 views.py 內定義的函數.
                    return ResolverMatch(sub_match.func,
                        sub_match.args, sub_match_dict,
                        sub_match.url_name, self.app_name or sub_match.app_name,
                        [self.namespace] + sub_match.namespaces)

                tried.append([pattern])

        # 若是沒有匹配成功的項目, 將異常
        raise Resolver404({'tried': tried, 'path': new_path})

    raise Resolver404({'path' : path})

# 修飾 urlconf_module, 返回 self._urlconf_module, 即 urlpatterns 變量所在的文件
@property
def urlconf_module(self):
    try:
        return self._urlconf_module
    except AttributeError:
        self._urlconf_module = import_module(self.urlconf_name)
        return self._urlconf_module

# 返回指定文件中的 urlpatterns 變量
@property
def url_patterns(self):
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
    try:
        iter(patterns) # 是否能夠迭代
    except TypeError:
        raise ImproperlyConfigured("The included urlconf %s doesn't have any patterns in it" % self.urlconf_name)

    # patterns 其實是 RegexURLPattern 對象和 RegexURLResolver 對象的集合
    return patterns

摘取 RegexURLPattern 的主幹函數做爲講解:

# 執行正則匹配
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.
        # match.groupdict() 返回正則表達式中匹配的變量以及其值, 須要瞭解 python 中正則表達式的使用
        kwargs = match.groupdict()
        if kwargs:
            args = ()
        else:
            args = match.groups()

        # In both cases, pass any extra_kwargs as **kwargs.
        kwargs.update(self.default_args)

        # 成功, 返回匹配結果類; 不然返回 None
        return ResolverMatch(self.callback, args, kwargs, self.name)

# 對 callback 進行修飾, 若是 self._callback 不是一個可調用的對象, 則可能仍是一個字符串, 須要解析獲得可調用的對象
@property
def callback(self):
    if self._callback is not None:
        return self._callback

    self._callback = get_callable(self._callback_str)
    return self._callback

ResolverMatch 不貼代碼了, 它包裝了匹配成功所須要的信息, 如 views.py 中定義的函數.

URL 調度過程實例

下面的具體例子將加深對 RegexURLResolver.reslove() 調用的理解. 假設工程名爲 mysite, 而且建立了 app people.this

# mysite.urls.py
from django.conf.urls import patterns, include, url

urlpatterns = patterns('',
     url(r"^$","mysite.views.index"),
     url(r"^about/","mysite.views.about"),
     url(r"^people/",include(people.urls)),
     url(r"^contact/","mysite.views.contact"),
     url(r"^update/","mysite.views.update"),
)

# people.urls.py
from django.conf.urls import patterns, include, url

urlpatterns = patterns('',
     url(r"^daoluan/","people.views.daoluan"),
     url(r"^sam/","people.views.sam"),
     url(r"^jenny/","people.views.jenny"),
)
# people.views.py
def daoluan(request):
    return HttpResponse("hello")

當訪問 http://exapmle.com/people/daoluan/ 的時候 URL dispatcher 的調度過程(藍色部分):
lua

urldispatcher_example

對應上面的例子 url 調度器機制的具體工做過程以下, 從 BaseHandler.get_response() 開始提及:

1. BaseHandler.get_response() 中根據 settings.py 中的 ROOT_URLCONF 設置選項構造 RegexURLResolver 對象, 並調用 RegexURLResolver.resolve("/people/daoluan/") 啓動解析, 其中 RegexURLResolver.regex = "^\", 也就是說它會過濾 "\", url 變爲 "people/daoluan/";

2. resolve() 中調用 RegexURLResolver.url_patterns(), 加載了全部的匹配信息以下(和圖中同樣):

  • (類型)RegexURLPattern (正則匹配式)[^$]
  • RegexURLPattern [^about/]
  • RegexURLResolver [^people/]
  • RegexURLPattern [^contact/]
  • RegexURLPattern [^update/]

語句 for pattern in self.url_patterns: 開始依次匹配. 第一個由於是 RegexURLPattern 對象, 調用 resolve() 爲 RegexURLPattern.resolve(): 它直接用 [^$] 去匹配 "people/daoluan/", 結果固然是不匹配.

3. 下一個 pattern 過程同上.

4. 第三個 pattern 由於是  RegexURLResolver 對象, 因此 resolve() 調用的是 RegexURLResolver.resolve(), 而非上面兩個例子中的 RegexURLPattern.resolve().  由於第三個 pattern.regex = "^people/", 因此會將 "people/daoluan/" 過濾爲 "daoluan/". pattern.resolve() 中會調用 RegexURLResolver.url_patterns(), 加載了全部的匹配信息以下(和圖中同樣):

  • RegexURLPattern [^daoluan$]
  • RegexURLPattern [^sam$]
  • RegexURLPattern [^jenny$]

語句 for pattern in self.url_patterns: 開始依次匹配. 第一個就中, 過程和剛開始的過程同樣. 所以構造 ResolverMatch 對象返回. 因而 BaseHandler.get_response() 就順利獲得 ResolverMatch 對象, 其中記錄了有用的信息. 在 BaseHandler.get_response() 中有足夠的信息讓你知道開發人員在 views.py 中定義的函數是 def daoluan(request): 在何時調用的:

# BaseHandler.get_response() 的定義
# 處理請求的函數, 並返回 response
def get_response(self, request):
    ......

    # 實例化 RegexURLResolver, 暫且將其理解爲一個 url 的匹配處理器, 下節展開
    resolver = urlresolvers.RegexURLResolver(r'^/', urlconf)
    ......

    # 調用 RegexURLResolver.resolve(), 能夠理解爲啓動匹配的函數; 返回 ResolverMatch 實例
    resolver_match = resolver.resolve(request.path_info)
    ......

    # resolver_match 對象中存儲了有用的信息, 譬如 callback 就是咱們在 views.py 中定義的函數.
    callback, callback_args, callback_kwargs = resolver_match
    ......

    # 這裏調用的是真正的處理函數, 咱們通常在 view.py 中定義這些函數
    response = callback(request, *callback_args, **callback_kwargs)
    ......

    return response

總結

從上面知道, url 調度器主要 RegexURLResolver, RegexURLPattern, ResolverMatch 和三個輔助函數 url(), include(), patterns() 完成. 能夠發現, url 的調度順序是根據 urls.py 中的聲明順序決定的, 意即遍歷一張表而已, 有沒有辦法提升查找的效率?

我已經在 github 備份了 Django 源碼的註釋: Decode-Django, 有興趣的童鞋 fork 吧.

搗亂 2013-9-15

http://daoluan.net

相關文章
相關標籤/搜索