并非魔术-flask和@app.route-第2部分

在上一篇文章中,我完成了一个框架,该框架模仿flask网站第一个示例的@app.route('/')行为。

在这篇文章中,我们将把难度提高一点点,并增加在url中使用可变参数的能力,到这篇文章结束时,我们将能够支持下面这段代码的预期行为。

app = Flask(__name__)

@app.route('/hello/<username>')
def hello_user(username):
    return "Hello {}!".format(username)

所以,以下路径:

/hello/ains

将匹配上面的路由并输出:

Hello ains!

正则表示路由

既然我们允许url是动态的,我们就不能再直接将我们提供的路径与之前使用“@app.route()”注册的路由进行比较了。

我们要怎么做呢?我们将需要使用正则表达式,这样我们就可以根据模式匹配路径,而不是将它们与固定的字符串进行比较。

在这篇博客文章中,我不会详细介绍正则表达式的细节,但是如果您需要复习一下,请访问这个网站

因此,我们的第一步是将路由转换为一个正则表达式模式,以便与传入的路径匹配。我们还将使用这个正则表达式来提取我们感兴趣的变量。

那么,匹配路由/hello/<username>的正则表达式是什么样的呢?

一个简单的正则表达式(例如^/hello/(.+)$)将是一个不错的开始,因此让我们看一下代码:

import re

route_regex = re.compile(r"^/hello/(.+)$")
match = route_regex.match("/hello/ains")

print(match.groups())

输出:

('ains',)

不过,在理想情况下,我们希望保留匹配的第一个组与路由/hello/<username>中的标识符“username”之间的链接。

命名捕获的组

幸运的是,正则表达式还支持命名捕获组,这使我们可以为匹配组分配名称,稍后在阅读匹配项时可以将其恢复。

我们可以使用以下语法为第一个示例中的捕获组提供用户名标识符。

/hello/(<?P<username>.+)"

然后,我们可以在正则表达式match上使用groupdict()方法来获取所有捕获组作为字典,并将该组的名称映射到匹配值。

现在我们得到以下代码:

route_regex = re.compile(r'^/hello/(?P<username>.+)$')
match = route_regex.match("/hello/ains")

print(match.groupdict())

将输出下面的字典:

{'username': 'ains'}

现在,有了我们需要的正则表达式的格式,以及如何使用它们来匹配传入的url的知识,剩下的就是创建一个方法,将我们声明的路由转换成它们等价的正则表达式模式。

为此,我们将使用另一个正则表达式(一直是正则表达式),将路由中的变量转换为正则表达式模式,例如,我们需要将<username>转换为(?P<username>.+)

听起来很简单!我们只用几行代码就可以做到。

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>'))

在这里,我们对所有出现的模式<\w+>(用尖括号括起来的字符串)进行正则表达式替换,并将其正则表达式称为组等效项。

在re.sub的第一个参数中,我们将模式<\w+>放在方括号内,以便将其分配给第一个匹配组。 在第二个参数中,我们可以通过写\1来使用第一个匹配组的内容(\2将是第二个匹配组的内容,依此类推,等等...)

最后,输入模式:

/hello/<username>

将返回正则表达式:

^/hello/(?P<username>.+)$

旧去新来

让我们快速查看一下我们上次构建的简单NotFlask类。

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!"

现在,我们有了一种新的和改进的匹配传入路由的方法,我们将不得不摆脱之前的简单字典功能。

让我们从修改添加路由的函数开始,这样我们将拥有一个(模式、视图函数)对列表,而不是将路由存储在字典中。

这意味着,当程序员用@app.route()装饰一个函数时,我们将尝试将他们的路由编译成一个正则表达式,然后将其与装饰后的函数一起存储在新路由列表中。

让我们来看看实现这一点的代码:

class NotFlask():
    def __init__(self):
        self.routes = []

    #这是我们之前建的build_route_pattern
    @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列表
            route_pattern = self.build_route_pattern(route_str)
            self.routes.append((route_pattern, f))

            return f

        return decorator

我们还需要一个get路由匹配方法,给定一个路径,它会尝试找到一个匹配的视图函数,如果找不到,则返回None。

不过,如果找到匹配,我们还需要返回一个东西,除了视图函数,还有之前匹配的捕获组的字典,我们需要这个来将正确的参数传递给视图函数。

这就是 get_route_match函数的样子:

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

现在我们差不多完成了,这个难题的最后一部分是如何从正则表达式匹配组的字典中调用具有正确参数的视图函数。

调用函数的一千种方法

让我们后退一步,看看在python中调用函数的不同方法。

比如这个例子:

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,我们可以很容易地将匹配字典作为参数传递给视图函数,完成NotFlask。

让我们把所有这些放到最终class中。

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))

现在,下面的片段就像魔术一样

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添加一点动态风格。

  • 上一篇: 第1部分
  • 0 条评论 最新

    还没有评论哦.