Last Updated: 2/6/2024, 5:44:57 AM

# Flask のポイントは、

グローバル変数です。

# はじめに

Flask は、あえてグローバル変数を使ったり、パッケージの階層構造を崩しています。 その分、簡単に書けるようになっていて人気を博しました。

Flask の作者である Armin Ronacher 氏は、 「冗談で作ったのになんでこんなに流行ってるのかわからない」と述べていました笑

ここでは一般に MVC で言われるところの C, コントローラ を中心に、 あえて何をしたのかを見ていきます。

具体的には以下のような用語や関数について、見ていきます。。

  • view
  • route
  • endpoint
  • redirect
  • blueprint

# ◯ ビューとモデル

なおビューとモデルは、触れません。

ビューについては、以下にまとめがありました。

モデルは、おそらく Flask-SQLAlchemy が該当すると思います。 Flask-SQLAlchemy は Flask で SQLAlchemy を使いやすいようにラップしたライブラリです。

Flask-SQLAlchemy と SQLAlchemy は、区別しておくのがいいかなと思います。 正直、自分は、あまり切り分けられていません笑

Flask の作者である Armin Ronacher 氏は、 Flask-SQLAlchemy について好意的に評価していました。

また Flask のサンプルコードでも、比較的よく使われているのを見かけるので、使ってもいいかなと思います。

# ◯ MVT

一般にウェブアプリケーションフレームワークは MVC モデルという言葉がよく使われます。 Flask では MVC ではなく MVT と呼ばれています。この記事でも MVT と呼ぶことにします。

ちなみに MVT とは Model, View, Template の略です。View は MVC の Controller, Template は MVC の View に該当します。ややこしいですね笑

なぜ、このように呼ばれているのかはわかりません。 公式ドキュメントには、MVT という記述はありません。

おそらく Contoller に当たる部分の関数を View と呼ぶことが原因かと思われます。 それに引きずられて View が Template になったのかなと思われます。

Blueprints and Views - Flask Tutorial (opens new window)
view 関数は、あなたのアプリケーションに来たリクエストに応答するために、 あなたが書いたコードです。 Flask は URL に来たリクエストを捌くための view がどれに対応するか調べるために、 パターンを使い対応します(訳者注: パターンについて下記参照)。
A view function is the code you write to respond to requests to your application. Flask uses patterns to match the incoming request URL to the view that should handle it.

以後、この記事では MVC の Controller に当たる部分を View, ビュー と呼び、 ビューに焦点を Flask を当てて掘り下げていきます。

# 1. ビュー, view

 リクエストを受けて、レスポンスを返す。  これがウェブアプリケーションの基本的な考え方です。 リクエストされると関数が呼び出されて、文字列 str を返しています。

自分の中で、意外とこの考えが曖昧になっていました。 以下のつぶやきは Django のつぶやきですが Flask も基本は同じです。 勉強になります。ありがとうございます。

# ◯ 正式名称

このとき呼び出される関数を Flask では、view と呼びます。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask

app = flask.Flask(__name__)

@app.route("/")
def index():  # <--- これがビューです。
    return "Hello World!"

app.run()

おそらく「パターン」とは route を登録するときに使う '/<username>', '/post/<int:post_id>' と言った表記を指しているのだと思われます。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

@app.route('/') # <--- これのことかと...
def index():
    return ''

@app.route('/<username>') # <--- これのことかと...
def show_user(username):
    return username

@app.route('/post/<int:post_id>') # <--- これのことかと...
def show_post(post_id):
    return post_id

app.run()


# URL Route Registrations - API
# http://flask.pocoo.org/docs/1.0/api/#url-route-registrations

# 2. ビュー - リクエスト

 リクエストを受けて、レスポンスを返す。  自分の中で、意外とこの考えが曖昧になっていて理解が苦しいところがありました。

では Flask は、どこからリクエストを受けているのでしょうか?

それは flask オブジェクトのflask.request 属性の中にはいっています。 リクエストされるたびに flask.request が更新されます。 例えば POST された内容は flask.request.form に保存されます。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask

app = flask.Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def index():
    if   flask.request.method == "GET":
        response = template
    elif flask.request.method == "POST":
        response = str(flask.request.form)
    return response                # ^^^^ ココに POST された
                                   #      内容がはいっています。


# FORM - HTMLタグリファレンス から拝借しました。
# http://www.htmq.com/html/form.shtml
template = """\
<form action="/" method="post">
  <p>
    名前:
    <input type="text" name="name" size="40">
  </p>
  <p>
    ご感想:
    <textarea name="kanso" rows="4" cols="40"></textarea>
  </p>
  <p>
    <input type="submit" value="送信">
    <input type="reset"  value="リセット">
  </p>
</form>
"""

app.run()

# (1) なんでグローバル変数なの?

なんでってなんでって話なのですが、 以下のような具合で request を引数で受けたらどうなんだろうって思いました。

#
# 偽物の flask
#
import flask

app = flask.Flask(__name__)

@app.route("/")
def index(request):  # <--- request を引数で受けたら
    if   request.method == "GET":
        response = template
    elif request.method == "POST":
        response = str(flask.request.form)
    return response

app.run()

# (2) デメリットはなに?

一般にプログラミングする際には、 グローバル変数は使わない方が良いとされています。

では Flask では、 request をグローバル変数 flask から受けてしまうことによって どのような影響が生じたのでしょうか?

  1. 大きなアプリケーションを組みづらくなります。
  2. 「コンテキスト」という理解しにくい概念が発生します。
  3. 「スレッドセーフ」を担保するために特別な実装が必要になります。

# 1. 大きなアプリケーションが組みづらい。

正直、何かしらコードを書いて示すことはできないのですが... 一般にグローバル変数を使って組み立ててしまうと、 大きなアプリを書きにくいことが多い気がします。

# 2. コンテキスト

コンテキストとは、簡単に言えばグローバル変数です。 Flask のポイントはコンテキストだと個人的に思っています。 コンテキストについては、次のページで見ていきます。

# 3. スレッドセーフ

スレッドセーフも context ってなに? の中で見ていきます。

# (3) メリットはなに?

なぜこのような欠点を受け入れてまで、このような実装をしたのでしょうか?

# 1. フック間の受け渡しが楽にいなる。

また第二に、フック間での受け渡しが楽になります。 これは公式ドキュメントのどこかで書かれていた気がするのですが、 現在捜索中です。

どういうことかと言うとグローバル変数 flaskrequest を入れなかった場合、  フック  の間で request オブジェクトを引数に渡さないといいけなくなり面倒になります。

フック (opens new window)

プログラムの中に独自の処理を割りこませるために用意されている仕組み。

もしくは

プログラムにおいて、本来の処理を横取りして独自の処理を割りこませること

です。

例えばどういう機能なのかというと、 例えば before_request (opens new window) を使うとビューが呼び出されたときに、 割り込んで処理を実行することができます。

以下のコードをコピペで実行して、/view1, /view2 にアクセスすると  コンソール  に 'ワロスwww' と表示されます。

import flask
app = flask.Flask(__name__)

@app.before_request
def before_request():
    print('ワロスwwww')

@app.route('/view1')
def view1():
    return 'view1'

@app.route('/view2')
def view2():
    return 'view2'

app.run()

Flask の場合は大量のフックを設ける関係で request については例外的にグローバルにしてしまったということなのでしょうか。 シグナルというのは、JavaScript などの言語で言えばイベントに該当します。

wsgi, Bottle, Flask の速度差 - Qiita (opens new window)
Flask は確かに、簡単なサンプルアプリを書くときの見た目はマイクロフレームワークになっています。 しかし、構造的には沢山のフック、シグナルがあったりしていて、重量級の設計になっています。 Flask 本体と Werkzeug を合わせると数万行のサイズです。 単なる Hello World アプリでも、  数十の関数呼び出しが裏で動いています。 

request を関数の間でリレーのバトン渡しのように、 渡し続けるのは面倒です。

# 引数でバトン渡しするコード
#     引数を書かないといけないので
#     面倒です。
def f(x):
    g(x)

def g(x):
    h(x)

def h(x):
    return x

f(0)
# グローバル変数を使ったコード
#     引数を書く必要がなくなり
#     少し楽になる。
def f():
    g()

def g():
    h()

def h():
    return x

x = 0

# 2. import を使ってアンパックできる。

まず第一に、Flask はグローバル変数を多用することで、とても手軽に書けるようになります。 request は、関数間でバケツリレーをする必要がなくなりました。

もう一方で注目したいのが sessiong です。 sessiong はリクエストごとに生成されます。

個人的な感想で言えば request.g, request.session として欲しかったです。 しかし Flask はそうはなっていません。なぜでしょうか。

おそらく request.g, request.session とは書きたくなかったからだと思います。 短く g, session と書きたかったからだと思います。

もし g, sessionrequest オブジェクトの属性にしてしまった場合、 これをアンパックするには、以下のようにしないといけません。これは面倒です。

#
# 偽物
#
from flask import request
g = request.g
session = request.session

Python ではイテラブルはアンパックできるのですが、 オブジェクト属性はアンパックできません。 例えば、標準ライブラリ datetime の datetime.now はアンパックできません。

now は関数のように見えます。なぜ import できないのでしょうか? それは now がdatetime クラスの クラスメソッド、 言い換えれば datetime クラスの属性だからです。

from datetime import datetime
datetime.now()
# datetime.datetime(2019, 12, 14, 13, 57, 23, 161417)

from datetime.datetime import now
>>> from datetime.datetime import now
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'datetime.datetime'; 'datetime' is not a package
>>>
class datetime(date):
    
    ...

    @classmethod
    def now(cls, tz=None):
        "Construct a datetime from time.time() and optional time zone info."
        t = _time.time()
        return cls.fromtimestamp(t, tz)

そこで Armin Ronacher 氏は、import 文を使いアンパックしていたというわけです。 flask モジュールがグローバル変数であれば import 文を使い、これをアンパックすることができます。

#
# 本物
#
from flask import request, session, g

最初はとても混乱して、意図が掴めませんでした。 パッケージの構成がぐちゃぐちゃ過ぎるだろうと思って意図が掴めませんでした。 しかし、Armin Ronacher 氏の動画を見て、 なるほどカジュアルに書けるようにすることを意図しているのかと腑に落ちました。

Flask-SQLAlchemy を好意的に評価していました。 自分は Flask-SQLAlchemy がなにをしているか、わからず、混乱していました。 ただ、やり方を知ってしまうと、とてもカジュアルに軽くかけます。 おそらく Armin Ronarcher 氏は、こういったカジュアルにかけるものが好きなのかなと思います。

# (4) Django と比較

以下は Django のコードです。 引数を明示しているので、リクエストを受けてレスポンスを返すというのがわかりやすいです。 型アノテーションという書き方で、リクエストとレスポンスをより明示した書き方をしました。

#
# Django のコードの一部です。
#
def view(request: HttpRequest) -> HttpResponse:
    template = loader.get_template(
        'app_name/template_name.html')
    context = {
        'blog_title' : 'やる夫の日記',
        'content'    : 'ケーキを食べた。'}
    response = HttpResponse(template.render(
        context,
        request))
    return response

# (5) Python と比較

Python そのものにもフックはたくさんあります。 Flask は、そう言ったフックを用意することは真似ました。 しかし、クラス定義時に self を書くようなこと、 request を書くことはしなかったということでしょうか。

#
# flask の選択
#   request を明示しない
#
import flask

app = flask.Flask(__name__)

@app.route("/")
def index():
    return flask.request.args.get('key')

app.run()
#
# 偽物の Flask
#   request を明示する。
#
import flask

app = flask.Flask(__name__)

@app.route("/")
def index(request):
    return request.args.get('key')

app.run()
#
# 偽物の Python
#   self を明示しない
#
class C:
    def f():
        self.attr = 'value'
#
# Python の選択
#   self を明示する
#
class C:
    def f(self):
        self.attr = 'value'

# 3. ビュー - レスポンス

 リクエストを受けて、レスポンスを返す。  自分の中で、意外とこの考えが曖昧になっていて理解が苦しいところがありました。

では Flask は何型、何クラスのオブジェクトを返しているのでしょうか?

Flask の view 関数を定義するときに私たちは return 文を書いています。 そこで書かれている return 文の書き方は、色々なやり方があります。

@app.route("/")
def view():
    # 1. str をそのまま返す。  
    return 'Hello, world!'

    # 2. flask.render_template を使う。
    return flask.render_template(
        'template', message='Hello, world!')

    # 3. flask.redirect を使う。
    return flask.redirect(
        flask.url_for('helloworld'))

ここで疑問なのは view 関数は、何型、何クラスのオブジェクトを返せばいいのでしょうか?

Blueprints and Views - Flask Tutorial (opens new window)
view は データを返します、 Flask はそのデータを応答のためにレスポンスに変換します (訳者注: HTTP ヘッダ等を付与することを指していると思われます)。 Flask は他にも view の名前と引数に基づいて URL を生成することもできます。
The view returns data that Flask turns into an outgoing response. Flask can also go the other direction and generate a URL to a view based on its name and arguments.

Flask の公式ドキュメントのクイックスタートに 一覧が載っていたので引用いたします。

レスポンスについて (opens new window)
About Response

ビュー関数からの返り値は、自動的にレスポンスオブジェクトに変換されます。
The return value from a view function is automatically converted into a response object for you.

もし返り値が文字列なら HTTP レスポンスのボディにその文字列が埋め込まれ、 ステータスコードに 200 OK と text/html に mimetype が指定された レスポンオブジェクトが生成されます。
If the return value is a string it’s converted into a response object with the string as response body, a 200 OK status code and a text/html mimetype.

もし返り値が辞書なら、jsonify() を呼び出してレスポンスオブジェクトを生成します。
If the return value is a dict, jsonify() is called to produce a response.

Flask アプリケーションがビュー関数の返り値をレスポンスオブジェクトに 変換する手順は以下の通りです。
The logic that Flask applies to converting return values into response objects is as follows:

  1. もしレスポンスオブジェクトがそのまま返されたなら ビュー関数は直接それを返します。
    If a response object of the correct type is returned it’s directly returned from the view.

  2. もし文字列なら、その文字列と事前に Flask に設定された値を 使ってオブジェクトを生成します。
    If it’s a string, a response object is created with that data and the default parameters.

  3. もし辞書なら、レスポンスオブジェクトは jsonify を使います。
    If it’s a dict, a response object is created using jsonify.

  4. タプルが返された場合、そのタプル内の項目は追加情報を提供できます。 そのようなタプルは、(レスポンス、ステータス)、(レスポンス、ヘッダー)、 または(レスポンス、ステータス、ヘッダー)の形式でなければなりません。 ステータス値はステータスコードを上書きし、 ヘッダーは追加のヘッダー値のリストまたは辞書になります。
    If a tuple is returned the items in the tuple can provide extra information. Such tuples have to be in the form (response, status), (response, headers), or (response, status, headers). The status value will override the status code and headers can be a list or dictionary of additional header values.

  5. それでもうまくいかない場合は、Flaskは戻り値が有効なWSGIアプリケーションであると見なし、 それを応答オブジェクトに変換します。
    If none of that works, Flask will assume the return value is a valid WSGI application and convert that into a response object.

# ◯ まとめ

Flask の view 関数はなんでも受け入れてくれる広い心の持ち主で、 とてもいいやつそうに見えます。

いい感じに自動的(暗黙的に)にやってくれるのは、 動的言語のメリットであると思います。 しかし、上記の説明は煩雑で煩わしいものであったのではないかなと感じます。

The Zen of Python - PEP 20 (opens new window)

  • 明示的であることは、暗黙的であるより良い。
    Explicit is better than implicit.

  • もし実装が説明困難なら悪い実装だ
    If the implementation is hard to explain, it's a bad idea.

では、なぜこのような 重箱の隅を突くような話を事細かにさせていただいたのかと言うと

それは後年、Armin Ronacher 氏は、 Flask ではないのですが、 Python の細かい部分での一貫性のない挙動、 暗黙的にいろんなことをやってくれる動作を嫌がっていた 気配のある記事を書いています。

Flask くらいならいいのでしょうがやはりより大きなものを作ろうとするときは、 きっと厳しく型を定めていかないといけないのかなと思ったりもします。

現在は Armin Ronacher は、より型システムの厳格な言語である Rust に軸足を移してしまいました。 id:methane 様のツイートで知りました。

# 4. ルーティング

ルートの書式の公式ドキュメントがどこにあるかわからなくて 探し待ってました笑

@app.route('/')  # <--- ここの部分の書き方の資料です笑
def index():
    return 'Hello World'

flask.Flask.route メソッドの公式の説明書はこちら。

# (1) 書式

route の書式の公式の説明書は、こちら。

string accepts any text without a slash (the default)
int accepts integers
float like int but for floating point values
path like the default but also accepts slashes
any matches one of the items provided
uuid accepts UUID strings

Django は 1 系から 2 系にバージョンアップするにあたり 正規表現から Flask ライクな形式に移行しました。 Armin Ronarcher のセンスすごいと思ってしまうわけです。

Django 2.0の変更点について - Qiita (opens new window)

従来の正規表現を用いた書き方(django.conf.urls.url())に加えて、 新たにシンプルな書き方(django.urls.path())ができるようになりました。

url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
path('articles/<int:year>/', views.year_archive),

# (2) デコレータ

デコレータ @ については、以下の記事で学びました。

ちなみに、ルートのディスパッチは線形探索らしいです。 辞書的な形で O(1) に近い形でやるのかなと思ったのですが、意外とここも単純でした。

# 5. エンドポイント, endpoint

Flask のエンドポイント, endpoint とはビュー, view に付与された文字列です。簡単に言えば、関数の名前です。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

# 1) 例えば
#    このビューのエンドポイントは
#    'view' になります。
@app.route('/')
def view():  
    # 2) また url_for を使えば
    #    エンドポイントから
    #    ルート相対パスを取得できます。
    return flask.url_for('view') 

app.run()

endpoint は識別子なんだ、この識別子を使って、 あなたのコードのどの部分がリクエストを捌くかを判別するんだ。
the "endpoint" is an identifier that is used in determining what logical unit of your code should handle the request.
What is an 'endpoint' in Flask? - Stakoverflow (opens new window)

# (1) url_for 関数

url_for 関数の引数に関数名 endpoint を渡すと ルート相対パス (opens new window) に変換します。 flask.redirect とセットで用いられたり、 あるいは template 内でも使われたりします。

以下、公式マニュアルから。

flask.url_for (opens new window)
指定された endpoint への URL を生成します。
Generates a URL to the given endpoint with the method provided.

url_for() 関数は名前と引数に基づいて URL を生成します。 ビュー関数と関連したこの名前のことを、 endpoint とも呼びます。
The url_for() function generates the URL to a view based on a name and arguments. The name associated with a view is also called the endpoint, and by default it’s the same as the name of the view function.
Endpoints and URLs - Flask (opens new window)

# (2) 問題

問題です。Flask では1つのビューに2つの URL を指定できます。 以下のコードを実行して

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

@app.route('/hello')
@app.route('/nihao')
def view():
    return flask.url_for('view')

app.run()

以下の URL にアクセスした時、表示される文字列は、 hello ですか?それとも nihao ですか?

たいした意味はないのですが、おそらく線形探索してるんだろうな、 くらいの雰囲気が伝わればと思いました。

# (3) endpoint を書き換える。

エンドポイントは基本的には関数名です。 しかし、キーワード引数 endpoint を使って変更できます。 いつ使うかは知りません。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

@app.route('/', endpoint='endpoint')
def view():
    return flask.url_for('endpoint')

app.run()

# (4) 引数付きの flask.url_for

url_for 関数には、エンドポイント以外の引数を渡せます。 以下のコードをコピペして実行して見てください。 print 文が何を出力しているか、見ていただけると幸いです。

「実行してブラウザにアクセスして下さい」は、さすがに面倒くさそうだったので、 test_request_context (opens new window) を使いました笑

また、なぜ with 文を使っているかは、このあと、コンテクストを説明する際に、 見ていきます。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

# 1)
@app.route('/view0/')
def view0():
    return ''

with app.test_request_context('/view0/?arg0=hello&arg1=nihao'):
    print(flask.request.args['arg0'])
    print(flask.request.args['arg1'])
    print(flask.url_for(
        'view0', arg0='hello', arg1='nihao',))


# 2)
@app.route('/view1/<string:arg0>')
def view1(parameter0):
    return ''

with app.test_request_context('/view1/hello?arg1=nihao'):
    print(flask.request.view_args['arg0'])
    print(flask.request.args['arg1'])
    print(flask.url_for(
        'view1', arg0='hello', arg1='nihao',))


# 3)
@app.route('/view2/<string:arg0>/<string:arg1>')
def view2(arg0, arg1):
    return ''

with app.test_request_context('/view2/hello/nihao'):
    print(flask.request.view_args['arg0'])
    print(flask.request.view_args['arg1'])
    print(flask.url_for(
        'view2', arg0='hello', arg1='nihao',))

# (5) 疑問: なんで URL じゃないの?

flask.redirect には2種類の書き方があります。 ルート相対パスを書く方法と、エンドポイントを各2種類の方法です。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

@app.route('/')
def index():
    # 1. ルート相対パスで
    #    リダイレクト
    return flask.redirect('/hello')

@app.route('/hello')
def hello():
    # 2. エンドポイントで
    #    リダイレクト
    return flask.redirect('nihao')

@app.route('/nihao')
def nihao():
    return 'Nihao, shijie!'

app.run()

なぜルート相対パス /hello でも書けるのに、 わざわざエンドポイント nihao を用意したのでしょうか?

理由はわかりません。 でも、1つのビュー関数に2つ以上の URL を定義できたりします。 url_for 関数を使うと、テンプレートを書くときに、 それなりに綺麗に書けたりするので、その辺りが意図かなと、 勝手に思い込んでいます。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

@app.route('/hello')
@app.route('/nihao')
def view():
    return flask.url_for('view')

app.run()

以下のリンク先は、2つの URL に複数のルートを定義している例です。 あと、全部の URL を捕まえる方法になります。 練習にはちょうど良いかも。

# (6) 疑問: なんで直接関数を渡さないの?

Flask は何かを特定するときに文字列を多用します。 例えば FLask クラスや Blueprint クラスもそうでした。

個人的には import しちゃえばよかったんじゃないかなと。 import すると identity で管理することになります。

公式チュートリアルからコードをちょっと引っ張ってきます。

以下のコードはログインしていなかったら 'auth.login' というエンドポイントを持つ ビューに飛ばしてくださいね、というコードです。

#
# 本物
#    エンドポイント、文字列で管理する
#
def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('auth.login'))

        return view(**kwargs)

    return wrapped_view

import させるなら、こんな感じになります。

#
# 偽物
#    identity で管理する。
#
import auth

def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for(auth.login))

        return view(**kwargs)

    return wrapped_view

文字列として参照してしまうのはアンチパターンな気がします。

1. メソッドやプロパティが文字列として参照されている - まずコードの可読性を最適化しよう (opens new window)

一部のフレームワークはこれをしがちです。 「コールバック」を文字列として渡し、必要に応じてリフレクションを使います。 この場合、古き良きCMD+Fを使いましょう。

最悪なフレームワークは、動的言語で、これらの文字列を動的にします。JavaScriptやActionScript 3.0がそうです。

文字列をエンドポイントとして使うことのメリットは import しなくてもいいことです。 そしてデメリットは、import してないので、どこで定義されているか、わからないということです。

そもそも Flask は大きいものは作らないよ、という設計思想なので、 どこで定義されてるかわかりにくいという指摘も、 もしかしたらお門違いなのかもしれません。

# 6. ブループリント, blueprint

# (1) 概要

blueprint は view を束ねたもの。app は blueprint を束ねたものです。 app > blueprint > view のような具合になります。

例えば以下のように app には無数の view がつけておくと混乱してしまいます。

app -------------  view, view, view, view, view, view, view, view

blueprint することが分割できます。

app -------------  view, view
 |---- blueprint - view, view
 |---- blueprint - view, view
 |---- blueprint - view, view

# (2) コード

例えば Flask の公式チュートリアルで作るブログアプリ flaskr (opens new window) は以下のような構成になっています。

app 
 |---- blog ------ index, create, delete
 |---- auth ------ register, login, logout

これをコードにすると、だいたい以下のようになります。 対話モード >>> にコピペで実行できます。

ただ、意味のあるコードというわけではありません。 実行して url_for が何を返すかを見るために、書きました。

@app.route('/') とするのではなく @blog_bp.route('/') とブループリントに登録している イメージだけつければと思いました。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

#
# blog_bp
#     ブログのブループリト
#
blog_bp = flask.Blueprint('blog', __name__, url_prefix='')
@blog_bp.route('/')
def index():
    return flask.url_for('blog.index')

@blog_bp.route('/create')
def create():
    return flask.url_for('blog.create')

@blog_bp.route('/delete')
def delete():
    return flask.url_for('blog.delete')

#
# auth_bp
#     認証用のブループリント
#
auth_bp = flask.Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/register')
def register():
    return flask.url_for('auth.register')

@auth_bp.route('/login')
def login():
    return flask.url_for('auth.login')

@auth_bp.route('/logout')
def logout():
    return flask.url_for('auth.logout')

app.register_blueprint(blog_bp)
app.register_blueprint(auth_bp)
app.run()

# (3) エンドポイント

通常はビュー関数の名前がエンドポイントになります。 view が app 直下ではなく blueprint に割り当てられた場合は、 エンドポイントは以下のようになります。

ブループリントの名前.関数の名前

例えば register というビュー関数のエンドポイントは、 ブループリントの名前 auth + ビュー関数の名前 registerauth.register となります。

auth_bp = flask.Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/register')
def register():
    return flask.url_for('auth.register')

# (4) ブループリントの引数

だいたい以下のような具合です。

auth_bp = flask.Blueprint(
    # 1) 名前
    name='auth',
    # 2) 位置
    import_name=__name__,
    # 3) 起点
    url_prefix='/auth'
)

# name

name はわかりやすいです。 ここで定義された name は配下のブループリントのエンドポイントの接頭辞になります。

また、クラス定義文を動的に書こうと思った時にも、 文字列でクラス名を書かないといけません。これと感覚的に近いかなと思います。

# 1 と 2 は等価です。

# 1
class C:
    pass

# 2
C = type('C', (object, ), {})

# import_url

import_name は何をしているか、さっぱり見当がつきません。 次の章で見ていきます。

# url_prefix

url_prefix も、まだわかります。 これを起点にしてルート相対パスが決まるのでしょう。

例えば ビュー関数 register への URL は url_prfix'/auth' + '/register''/auth/register' となります。

auth_bp = flask.Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/register')
def register():
    return flask.url_for('auth.register')

# 7. Flask の __main__ ってなに?

# (1) 疑問

いつも思うんです。この __name__ ってなに?って

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app       = flask.Flask(                 __name__)
blueprint = flask.Blueprint('blueprint', __name__)
                                       # ^^^^^^^^ これです
@blueprint.route('/')
def index():
    return 'Hello, world!'

app.register_blueprint(blueprint)
app.run()

# (2) 答え

template や static ディレクトリがどこに置かれているかを判別するために、 使うそうです。

Quickstart - Flask (opens new window)
これは Flask がテンプレートや静的ファイルなどがどこに置かれているかを 知るのに必要です。 より詳しくは Flask (opens new window) のドキュメントをご確認ください。
This is needed so that Flask knows where to look for templates, static files, and so on. For more information have a look at the Flask (opens new window) documentation.

このことについては、以下のリンクから気づくことができました。

あとリンク先で簡単にソースコードを追いかけてくれています。

# (3) __name__ ってなに?

あれ?__name__ には __main__ が代入されるんじゃないの?と疑問に思われるかもしれません。 実は、それは「スクリプト」として実行された時だけです。

「モジュール」、「パッケージ」として import された時は、 「スクリプト」からの相対パスが代入されます。

例えば、以下の様なディレクトリ階層について

$ tree
.
├── script.py
└── package0
    ├── __init__.py
    ├── module1.py
    └── package1
        ├── __init__.py
        └── module2.py

2 directories, 5 files
$ 

各々 __name__ を表示させて見ると以下の様になります。

$ python3 script.py 
__file__: script.py
__name__: __main__
__file__: /Users/user/__name__/package0/__init__.py
__name__: package0
__file__: /Users/user/__name__/package0/module1.py
__name__: package0.module1
__file__: /Users/user/__name__/package0/package1/__init__.py
__name__: package0.package1
__file__: /Users/user/__name__/package0/package1/module2.py
__name__: package0.package1.module2
$ tree

__name__ (opens new window)
__name__ 属性はモジュールの完全修飾名に設定されなければなりません。 この名前を利用してインポートシステムでモジュールを一意に識別します。

__main__ (opens new window)
'__main__' はトップレベルのコードが実行されるスコープの名前です。 モジュールが、標準入力から読み込まれたとき、スクリプトとして実行されたとき、 あるいはインタラクティブプロンプトのとき、__name__ には '__main__' が設定されます。

今回使用したサンプルコードはこちらにあります。

import は意外に難しいのです。 以下の記事で説明させていただきました。 2つがポイントです。 パスの概念。「スクリプト」、「モジュール」、「パッケージ」の3つの違いを理解することです。

# (4) もうちょっと詳しく

公式マニュアルと調べてみると import_name という名前の引数らしいです。 これはなにものでしょうか?

class flask.Flask(          import_name, ...
class flask.Blueprint(name, import_name, ...

import name - アプリケーションパッケージの名前
import_name - the name of the application package
flask.Flask - Flask (opens new window)

該当箇所を引っ張ってきました。 公式ドキュメントによる とtemplate や static ディレクトリの場所を 指定するだけではなくデバッグ時にも必要らしいです。

About the First Parameter - Flask (opens new window)
The idea of the first parameter is to give Flask an idea of what belongs to your application. This name is used to find resources on the filesystem, can be used by extensions to improve debugging information and a lot more.

So it’s important what you provide there. If you are using a single module, __name__ is always the correct value. If you however are using a package, it’s usually recommended to hardcode the name of your package there.

For example if your application is defined in yourapplication/app.py you should create it with one of the two versions below:

app = Flask('yourapplication')
app = Flask(__name__.split('.')[0])

Why is that? The application will work even with __name__, thanks to how resources are looked up. However it will make debugging more painful. Certain extensions can make assumptions based on the import name of your application.

For example the Flask-SQLAlchemy extension will look for the code in your application that triggered an SQL query in debug mode. If the import name is not properly set up, that debugging information is lost. (For example it would only pick up SQL queries in yourapplication.app and not yourapplication.views.frontend)

# (5) FastAPI

FastAPI などの同等のマイクロフレームワークで確認しました。 しかし、見当たりません。 デバッグ時でも必ずしも必要なものではないのかもしれません...

#
# FastAPI のコード
#
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}
$ # app.run() は見当たらないので
$ # uvicorn という ASGI をインストールして実行する様子
$ pip install uvicorn
$ uvicorn main:app --reload

静的ファイルを追加する時は app.mount でディレクトリを明示する様子です。

#
# FastAPI のコード
#
from fastapi import FastAPI
from starlette.staticfiles import StaticFiles

app = FastAPI()

app.mount("/static", StaticFiles(directory="static"), name="static")

それにしても __name__ は必要なさそう... __name__ などの文字列 str を使って何かを識別しようとするのは、 Flask の特徴の様な気がします。

# 8. リンク

読んだわけではないのですが、リンクを集めました。

# ◯ いまさらながら Flask についてまとめる

  1. はじめに (opens new window)
  2. Routing (opens new window)
  3. Template (opens new window)
  4. Logging (opens new window)
  5. Blueprint (opens new window)
  6. エラーハンドリング (opens new window)

# ◯ そのほか

# おわりに

当初、個人的に Flask の辛いかなと感じたのは、以下2点です。

  1. グローバル変数を多用する(例えば flask.request)
  2. ライブラリの階層がよくわからない(例えば flask.request, flask.session, flask.g)

そもそも何がわからないかも、わかりませんでした。 全体像が掴みづらく、辛かったのを覚えています。

ただ、いろいろ触っていて感じたのは、 Flask は、本当に気軽にかけるような作りになっています。 Armin Ronacher 氏が動画が冗談で作ったのに、 なんでこんなに Flask が流行ってるのかわからないんだよねと動画の中で言っていました。

その動画を見ていてやっと、ああ、そういうことか、となんとなく腑に落ちた感じがしました。 冗談という軽さもありますが、作者の心遣いみたいなのが、伝わってきました。

ここまで、以下の流れで見てきました。

Flask の外側の機能、API についておさらいしました。 次の記事では、Flask の内側を、コンテキストを軸にして見ていきたいと思います。 以上になります。ありがとうございました。