Flaskで動的にrouteを追加する

2023-01-07

forループを使ってrouteを追加する際のめもです。

(ついでに The Hitchhiker’s Guide to Python の Common Gotchas 1のメモも追記しています。)


最小限の最小限のアプリケーション2を準備します。

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

新しく/popcornと/creamsoadの2つのルートを追加することを考えます。

一般的な方法

それぞれ個別で定義する方法です。 route()3 デコレーターを使うと以下のように追加することになります。

@app.route('/popcorn')
def popcorn():
    return '🍿 popcorn'

@app.route('/creamsoad')
def creamsoad():
    return '🥤 cream soad'

この方法でも特に問題はありませんが、単純だったり、重複的な場合は一気に作成したくなる場合があります。

loopを使って追加する

forのloop内でview関数を定義し、add_url_rule()4 を使って追加していきます。

from functools import partial


s = {'popcorn': '🍿 popcorn', 'creamsoad': '🥤 cream soad'}

for key, value in s.items():

    def temp_func(v):
        return v

    view_func = partial(temp_func, v=value)

    app.add_url_rule(f'/{key}', endpoint=key, view_func=view_func)

実際にapp.run()で起動させ、httpコマンド 5 を使用してアクセスできるか確認します。

$ http http://127.0.0.1:5000/popcorn
HTTP/1.1 200 OK
Connection: close
Content-Length: 12
Content-Type: text/html; charset=utf-8
Date: Xxx, xx Xxx 2022 xx:xx:xx GMT
Server: Werkzeug/2.2.2 Python/3.9.9

🍿 popcorn

/popcornへアクセスできることが確認できました。

以下ではおまけとしてクロージャの遅延評対策とエンドポイントについて記載しています。

Late Binding Closures

上記例では、add_url_ruleでview関数を登録する際にfunctools.partial 6 を使用しています。

これは Late Binding Closures (クロージャの遅延評価)を掻い潜る(?)ために必要です。

もしpartialを使用しない場合は以下のようになります。

s = {'popcorn': '🍿 popcorn', 'creamsoad': '🥤 cream soad'}

for key, value in s.items():

    def temp_func():
        return value

    app.add_url_rule(f'/{key}', endpoint=key, view_func=temp_func)

s.items()で与えられたvalueをtemp_funcの引数ではなくそのまま使用しています。

/popcornにアクセスしてみます。

$ http http://127.0.0.1:5000/popcorn
HTTP/1.1 200 OK
Connection: close
Content-Length: 15
Content-Type: text/html; charset=utf-8
Date: Xxx, xx Xxx 2022 xx:xx:xx GMT
Server: Werkzeug/2.2.2 Python/3.9.9

🥤 cream soad

期待される 🍿 popcorn ではなく 🥤 cream soad が返ってきました。

これはクロージャで変数をバインドすることで発生しています。

(今回であればクロージャtemp_func内でvalueを使用しています。/popcornにアクセスし、view_func(=temp_func)valueを返す時にバインドされている"cream soda"が返されてしまいます。)

簡単な例

より簡単な例は以下のようになります。

fs = [lambda : n**2 for n in [1, 2, 3]]
print([f() for f in fs])

1, 2, 3の2乗が返されるように見えるため[1, 4, 9]を考えますが、実際には[9, 9, 9]が返ってきます。

Pythonのクロージャにおける遅延バインディングは "The Hitchhiker’s Guide to Python" などで詳しく確認できます7

functools.partialを使用することで引数が評価され期待通りに動作します。

import functools
import math

fs = [functools.partial(math.pow, n) for n in [1, 2, 3]]
print([f(2) for f in fs])  # [1.0, 4.0, 9.0]

関数のデフォルト引数が関数が定義されるタイミングで評価されることを利用することもできます8

キャッシング機能を追加したりする際にも利用できますし、今回のようなケースで先に評価してもらうことで期待通り動作します。

fs = [lambda i=n: i**2 for n in [1, 2, 3]]
print([f() for f in fs])  # [1, 4, 9]

ただ、個人的にはfunctools.partialの方が意図を汲みやすい気がするので使っています。

endpoint

上記例ではadd_url_ruleをする際にendpoint=keyのようにエンドポイントを指定しています。


add_url_ruleにおいて、view_functionが登録される場所を簡略的に表すと以下のようになります。 (詳しくはflask/app.pyを確認してください。)

    ...
    def add_url_rule(self,
            rule, endpoint=None, view_func=None, 
            provide_automatic_options=None, **options):
        if endpoint is None:
            endpoint = view_func.__name__

        ...

        self.view_functions[endpoint] = view_func
    ...

endpointが存在しない場合は.__name__を使用して関数の名前を設定するようにしています。

しかし、partialを使用した場合は.__name__を使用することができずエラーを吐きます。

そのため、endpoint=keyのように指定する必要があります。

AttributeError: 'functools.partial' object has no attribute '__name__'

同じendpointに対して複数の登録が確認されると、エラーが発生します。(flask/scaffold.py_endpoint_from_view_funcにて)

例えばpartialを使用せずview_functionにtemp_funcを使用し、endpointを指定しなかった場合などです。

AssertionError: View function mapping is overwriting an existing endpoint function: temp_func

最終的にResponseとしてview_functions内の関数がendpointをkeyとして返されます。

ルーティング

実際にappに登録されているルーティング等を確認したい場合は

などがあります。


app.view_functions

{
    'static': <function Flask.__init__.<locals>.<lambda> at 0x1045d2820>, 
    'popcorn': functools.partial(<function temp_func at 0x1045e5310>, v='🍿 popcorn'), 
    'creamsoad': functools.partial(<function temp_func at 0x1045e53a0>, v='🥤 cream soad')}

app.url_map

Map([
    <Rule '/static/<filename>' (OPTIONS, HEAD, GET) -> static>,
    <Rule '/popcorn' (OPTIONS, HEAD, GET) -> popcorn>,
    <Rule '/creamsoad' (OPTIONS, HEAD, GET) -> creamsoad>])

参考文献