適当おじさんの適当ブログ

技術のことやゲーム開発のことやゲームのことなど自由に雑多に書き連ねます

いまさらながら Flask についてまとめる 〜Blueprint〜

はじめに

いまさらながら Flask について整理していきます。「Flaskとかいうやつを使って、試しにアプリ開発にトライしてみたい」くらいの感覚の人を対象にしています。

Flaskのバージョンは 0.12.2 です。

この記事では、Blueprint について紹介していきます。

Blueprints?

Blueprintとは、アプリケーションの機能を分割して実装するためのものです。公式ドキュメントでは、大きなプロジェクトを整理するための方法としてBlueprintが推奨されています。

Blueprintを用いた実装は、以下の2ステップで行われます。

  1. Blueprintを実装する
  2. 実装したBlueprintをFlaskのアプリケーションに登録する
簡単な例

Blueprintを使わないのであれば、以下のように views.py に関数を定義していくかと思います。

# views.py
from flask import Flask

# Flaskのアプリケーションオブジェクト
app = Flask(__name__)

@app.route('/func1/a')
def func1_a():
    return 'func1_a'

@app.route('/func1/b')
def func1_b():
    return 'func1_b'

@app.route('/func2/a')
def func2_a():
    return "func2_a"

if __name__ == '__main__':
    app.run(debug=True)

views.py の func1 と func2 が異なる機能の塊であるとみなし、Blueprintに置き換えます。func1/views.py と func2/views.py というファイルを作成し、それぞれBlueprintを実装します。

/FlaskAppDirectory
├── app.py
├── func1
│     ├──__init__.py
│     └── views.py
└── func2
      ├──__init__.py
      └── views.py
# func1/views.py
from flask import Blueprint

# func1のBlueprint
func1 = Blueprint('func1', __name__, url_prefix='/func1')

@app.route('/a')
def func1_a():
    return 'func1_a'

@app.route('/b')
def func1_b():
    return 'func1_b'
# func2/views.py
from flask import Blueprint

# func2のBlueprint
func2 = Blueprint('func2', __name__, url_prefix='/func2')

@app.route('/a')
def func2_a():
    return 'func2_a'

続いて、作成したBlueprintをflask.register_bluepirnt()でアプリケーションに登録します。なお、Blueprintの登録を解除する関数はありません。つまり、一度登録したBlueprintを動的に削除することはできません。

# app.py
from flask import Flask
from func1.views import func1
from func2.views import func2

app = Flask(__name__)
# blueprintをアプリケーションに登録
app.register_blueprint(func1)
app.register_blueprint(func2)

if __name__ == '__main__':
    app.run(debug=True)

これで、http://xxx/func1/ahttp://xxx/func1/bhttp://xxx/func2/a にアクセスできるようになっています。

この例だけ見ると、「views.pyに定義されている関数を別のファイルに分けるだけの機能」に見えてしまうかもしれませんが、そうではありません。Blueprintごとに、テンプレートや静的ファイルなどの設定もできます。テンプレートと静的ファイルに関する設定については後述します。

設定項目の詳細が知りたい場合は、公式ドキュメントを参照ください。

テンプレートの走査パスの追加

Flaskではrender_template()実行時にtemplatesディレクトリ以下に対象のテンプレートがあるかどうかを確認します。ファイルがあればレンダリングして返し、なければTemplateNotFoundの例外が投げられます。

Blueprintのtemplate_folderオプションで、テンプレートを走査するディレクトリを追加できます。たとえば、以下のようにそれぞれのBlueprintで対象のディレクトリを追加できます。

# func1/views.py
func1 = Blueprint('func1', __name__, url_prefix='/func1', template_folder='func1_templates')

# func2/views.py
func2 = Blueprint('func2', __name__, url_prefix='/func2', template_folder='func2_templates')

アプリケーションの構成は以下のようなものを想定しています。各ディレクトリにtemplatesディレクトリとテンプレートを追加しています。

/FlaskAppDirectory
├── app.py
├── func1
│     ├── __init__.py
│     ├── views.py
│     └── func1_templates
│            └── func1_a.html
└── func2
     ├── __init__.py
     ├── views.py
     └── func2_templates
             └── func2_a.html

このtemplate_folderの指定は、Blueprintで閉じたものではありません。つまり、func2.py で func1_templates 以下のファイルを指定して render_template() を実行してもTemplateNotFoundは発生せずレンダリングされます。てっきりBlueprintごとに設定されるのだと思っていましたが、そうではありません。この挙動を受けて、「テンプレートの走査パスの追加」という題目にしています。

Blueprintの構成例

公式ドキュメントでは以下のようなディレクトリ構成で記載されています。

/FlaskAppDirectory
├── app.py
└── blueprints
    └── func1
        ├── __init__.py
        ├── views.py
        └── templates
             └── func1
                    └── func1_a.html
# func1/views.py
import os
from flask import Blueprint, render_template

func1 = Blueprint('func1', __name__, url_prefix='/func1', template_folder='templates')

@func1.route('/a')
def func1_a():
    return render_template(os.path.join(func1.name, 'func1_a.html'))

このサンプルのように、テンプレートを配置するディレクトリはすべて templates としたほうがわかりやすいように思います。また、templates以下に機能名のディレクトリを1つ作り、その配下にテンプレートを配置したほうが良いです。templates以下に同じ到達パス、かつ、同じファイルがあるとrender_template()で問題が発生する可能性があるためです。

静的ファイルの走査パスとURLの追加

テンプレート同様、Blueprint生成時のオプションで静的ファイルのパスを追加できます。静的ファイルのパスは、static_folderオプションで追加することができます。

以下の func3 をregister_blueprint()で登録すると、 http://xxx/func3/static のRoutingが定義され、アクセスできるようになります。

# func3.py
func3 = Blueprint('func3', __name__, url_prefix='func3', static_folder='./static')
/FlaskAppDirectory
├── app.py
├── static
└── func3
    ├── __init__.py
    ├── static
    │   └── test.css
    ├── templates
    └── view.py
URL生成ルールについて

url_prefixstatic_url_path の組み合わせで以下のようなルールで http://xxx/ 以降のURLが生成されます。

url_prefix static_url_path 結果
なし なし なし
あり なし {url_prefix}/{static_folder}
なし あり {static_url_path}
あり あり {url_prefix}{static_url_path}

注目すべきは4つ目の「あり」「あり」の状態です。url_prefixstatic_url_pathの間に/がありません。url_prefix = astatic_url_path = bの場合、'/ab'となります。つまり、この2つの値はパス結合ではなく、単純に文字列結合されています。

私はurl_prefixありのパターンを使っています。機能の分割を目的としているので、生成されるURLも機能ごとに分離しておくのが筋だと考えています。url_prefixが設定されていれば、静的ファイルのURLが被りにくくなるとも思います。

以下のようなケースでURLが被ってしまった場合、static/test.css が優先され、func3/static/test.css には到達できません。

# func3.py
func3 = Blueprint('func3', __name__, static_folder='./static', static_url_path='/static')
/FlaskAppDirectory
├── app.py
├── static
│   └── test.css
└── func3
    ├── __init__.py
    ├── static
    │   └── test.css
    ├── templates
    └── view.py

func3以下のstaticのディレクトリ名を別名に変更することで回避可能です。しかし、静的ファイル置き場=static と名前が統一されている方がわかりやすいと思います。以上から、url_prefixstatic_url_pathでURLを設定するのがシンプルで良いのではないでしょうか。

アプリケーションオブジェクトの取得

「Blueprintにはなく、アプリケーションオブジェクトにある機能を使いたい」という場合があります。たとえば、Loggerの取得です。

Blueprintでない場合、Loggerは以下のように取得できます。

from flask import Flask                                                         
                                                                                  
app = Flask(__name__)                                                                                                                          
                                                                            
@app.route('/')                                                                 
def index():                                                                    
    app.logger.debug('debug')
    return 'Flask case'

Blueprintでも同様にログ出力をしたいケースがありますが、BlueprintはLoggerを持っていません。なので、Flaskのアプリケーションオブジェクトから Logger を取得しなければなりません。

current_app でFlaskのアプリケーションオブジェクトを取得できます。つまり、Loggerはcurrent_app.loggerで取得できます。

from flask import Blueprint, current_app

func4 = Blueprint('func4', __name__)

@func4.route('/')
def index():
    logger = current_app.logger
    logger.debug('debug')
    return 'Blueprint case'