在我上一篇文章,我搭了一個框架,模擬了Flask網站上“@app.route(‘/’)”第一條例子的行為。
如果你錯過了那篇“這不是魔法”,請點選這裡。
在這篇文章中,我們打算稍微調高點難度,為我們的URL加入可變引數的能力,在本文的最後,我們將支援下述程式碼段所期望達到的行為。
1 2 3 4 5 |
app = Flask(__name__) @app.route("/hello/<username>") def hello_user(username): return "Hello {}!".format(username) |
這樣下面的路徑例項(path):
/hello/ains
將會匹配上面的路徑,給我們的輸出為
Hello ains!
以正則的形式表達我們的路徑。
現在我們將允許我們的URL動態變化,我們不再能夠將用先前使用“@app.route()”註冊的路徑直接與路徑例項比較。
我們將用什麼替代?我們需要用上正規表示式,這樣我們就可以將路徑作為一種模式進行匹配,而不和一條固定的字串比較了。
我不打算在本文展開討論正規表示式的細節,不過如果你需要一份複習資料,可以點選這個網站。
那麼,我們的第一步是將我們的路徑轉化成正規表示式模式,這樣我們就能在輸入路徑例項時進行匹配。我們也將使用這個正規表示式提取我們感興趣的變數。
那麼,匹配路徑”/hello/”的正規表示式該長啥樣呢?
嗯一個簡單的正規表示式譬如“^/hello/(.+)$”將是個好的開始,讓我們一起看看它和程式碼是怎麼一起工作的:
1 2 3 4 5 6 |
import re route_regex = re.compile(r"^/hello/(.+)$") match = route_regex.match("/hello/ains") print match.groups() |
將會輸出:
('ains',)
不錯,不過,理想情況是我們想要維護我們已經匹配上的第一組連結,並且從路徑“/hello/”識別出“username”。
命名捕獲組
幸運的是,正規表示式也支援命名捕獲組,允許我們給匹配組分配一個名字,我們能在讀取我們的匹配之後找回它。
我們可以使用下述符號,給出第一個例子識別“username”的捕獲組。
1 |
/hello/(<?P<username>.+)" |
然後我們可以對我們的正規表示式使用groupdict()方法,將所有捕獲組當作一個字典,組的名字對應匹配上的值。
那麼我們給出下述程式碼:
1 2 3 4 |
route_regex = re.compile(r'^/hello/(?P<username>.+)$') match = route_regex.match("/hello/ains") print match.groupdict() |
將為我們輸出以下字典:
{'username': 'ains'}
現在,有了我們所需要的正規表示式的格式,以及如何使用它們去匹配輸入的URLs的知識,最後剩下的是寫一個方法,將我們宣告的路徑轉換成它們等價的正規表示式模式。
要做這個我們將使用另一個正規表示式(接下來將全是正規表示式),為了讓我們路徑中的變數轉換成正則表示式模式,那這裡作為示範我們將將“”轉換成“(?P.+)”。
聽起來太簡單了!我們將可以只用一行新程式碼實現它。
1 2 3 4 5 |
def build_route_pattern(route): route_regex = re.sub(r'(<\w+>)', r'(?P\1.+)', route) return re.compile("^{}$".format(route_regex)) print build_route_pattern('/hello/<username>') |
這裡我們用一個正規表示式代表所有出現的模式(一個包含在尖括號中的字串),與它的正規表示式命名組等價。
re.sub的第一個引數 我們將我們的模式放進括號,目的是把它分配到第一個匹配組。在我們的第二個引數,我們可以使用第一匹配組的內容,方法是寫1(2將是第二匹配組的內容,以此類推…….)
那麼最後,輸入模式
1 |
/hello/<username> |
將給我們正規表示式:
1 |
^/hello/(?P<username>.+)$ |
推陳出新
讓我們掃一眼上次我們寫的簡單NotFlask類。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class NotFlask(): def __init__(self): self.routes = {} def route(self, route_str): def decorator(f): self.routes[route_str] = f return f return decorator def serve(self, path): view_function = self.routes.get(path) if view_function: return view_function() else: raise ValueError('Route "{}"" has not been registered'.format(path)) app = NotFlask() @app.route("/") def hello(): return "Hello World!" |
現在我們有一個新的改進方法用來匹配輸入的路徑,我們打算移除我們上一版實現時用到的原生字典。
讓我們從改造我們的函式著手,以便於新增路徑,這樣我們就可以用(pattern, view_function)對列表代替字典儲存我們的路徑。
這意味著當一個程式設計師使用@app.route()裝飾一個函式,我們將要嘗試將他們的路徑編譯變成一個正規表示式,然後儲存它,屬於一個在我們新的路徑列表裡的裝飾函式。
讓我們看看實現程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class NotFlask(): def __init__(self): self.routes = [] # Here's our build_route_pattern we made earlier @staticmethod def build_route_pattern(route): route_regex = re.sub(r'(<\w+>)', r'(?P\1.+)', route) return re.compile("^{}$".format(route_regex)) def route(self, route_str): def decorator(f): # Instead of inserting into a dictionary, # We'll append the tuple to our route list route_pattern = self.build_route_pattern(route_str) self.routes.append((route_pattern, f)) return f return decorator |
我們也打算需要一個get_route_match方法,給它一個路徑例項,將會嘗試並找到一個匹配的view_function,或者返回None如果一個也找不到的話。
然而,如果找了到匹配的話,除了view_function之外,我們還需要返回一個東西,那就是我們包含之前捕獲匹配組的字典,我們需要它來為檢視函式傳遞正確的引數。
好了我們的get_route_match大概就長這樣:
1 2 3 4 5 6 7 |
def get_route_match(path): for route_pattern, view_function in self.routes: m = route_pattern.match(path) if m: return m.groupdict(), view_function return None |
現在我們快要完成了,最後一步將是找出呼叫view_function的方法,使用來自正規表示式匹配組字典的正確引數。
呼叫一個函式的若干種方法
讓我們回顧一下不同的方法呼叫一個python的函式。
比如像這樣:
1 2 |
def hello_user(username): return "Hello {}!".format(username) |
最簡單的(也許正是你所熟知的)辦法是使用正則引數,在這裡引數的順序匹配我們定義的那些函式的順序。
>>> hello_user("ains")
Hello ains!
另一種方法呼叫一個函式是使用關鍵詞引數。關鍵詞引數可以通過任何順序指定,適合有許多可選引數的函式。
>>> hello_user(username="ains")
Hello ains!
在Python中最後一種呼叫一個函式的方法是使用關鍵詞引數字典,字典中的關鍵詞對應引數名稱。我們告訴Python解包一個字典,並通過使用兩個星號“**”來把它當作函式的關鍵詞引數。 下面的程式碼段與上面的程式碼段完全一樣,現在我們使用字典引數,我們可以在執行時動態建立它。
>>> kwargs = {"username": "ains"}
>>> hello_user(**kwargs)
Hello ains!
好了,還記得上面的groupdict()方法?就是那個同樣的在正規表示式完成匹配後返回{“username”: “ains”}的傢伙?那麼現在我們瞭解了kwargs,我們能很容易向我們的view_function傳遞字典匹配,完成NotFlask!
那麼讓我們把這些都塞進我們最終的類中。
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 |
class NotFlask(): def __init__(self): self.routes = [] @staticmethod def build_route_pattern(route): route_regex = re.sub(r'(<\w+>)', r'(?P\1.+)', route) return re.compile("^{}$".format(route_regex)) def route(self, route_str): def decorator(f): route_pattern = self.build_route_pattern(route_str) self.routes.append((route_pattern, f)) return f return decorator def get_route_match(self, path): for route_pattern, view_function in self.routes: m = route_pattern.match(path) if m: return m.groupdict(), view_function return None def serve(self, path): route_match = self.get_route_match(path) if route_match: kwargs, view_function = route_match return view_function(**kwargs) else: raise ValueError('Route "{}"" has not been registered'.format(path)) |
接下來,就是見證奇蹟的時刻,請看下面程式碼段:
1 2 3 4 5 6 7 |
app = NotFlask() @app.route("/hello/<username>") def hello_user(username): return "Hello {}!".format(username) print app.serve("/hello/ains") |
我們將得到輸出:
Hello ains!
結語
好了這就是“這不是魔法”對Flask的app.route()的深入探討。它證明我們所需要做的只是用一點正規表示式和Python使用關鍵詞引數呼叫函式的方法,用來給我們的URL增加一點動態性。
NotFlask例子的原始碼,以及配套的微型測試套件已經放在Github上請前去檢視。
本系列的下一篇我將會深入研究AngularJS,看看它有多依賴注射,以及如何實現下面程式碼段的宣告!
1 2 3 4 5 |
angular.module('test', ['$http', function($http) { $http.get("http://google.com/").success(function(data) { console.log(data); }); }]); |