AI can fly !!

AI がやりたい Web エンジニアのアウトプット (AI の知識は無い)

【Flask】ディレクトリ構成を考える

flask-logo

追記

- 2020/09/13 -

コメント欄で msiz07 様からご指摘頂きました通り、当記事で Flask 公式ドキュメント としている参照先はすべて、正確には Flask 公式ドキュメント (本家英語版) を個人の方が和訳したもの になります。

厳密に言えば参照先は 非公式 となりますが、内容に関しては本家の英語版と同様に大変参考になるものですので、非公式であるという注釈を追記した上で引続き参照させていただいております。

msiz07 様、コメントありがとうございました!


Flask 公式ドキュメント (本家英語版)

flask.palletsprojects.com

はじめに

Flask は Python 用 Web アプリケーションフレームワークの一つで、自身をマイクロフレームワーク (microframework) と呼んでいます。

Flask 公式ドキュメント ( *1 ) にもある通り、マイクロフレームワークの意味するところは、必要最小限の機能を提供しつつも多くの拡張性を保持することを目的としており、シンプルが故に機能が不足するということではありません。

シンプルなのは良いことですが、同じ Python 用 Web アプリケーションフレームワークである Django のように決まったディレクトリ構造を持たないため、目的に応じて自由にディレクトリやファイルを構成できる利点はあるものの、一定規模以上の Web アプリを作成しようとしたり複数名での開発を行う場合はある程度決まったルールが必要です。

ということで、今回はユースケースに応じた Flask アプリケーションのディレクトリ構成を考えてみました。

TL;DR

  • 必要なことはすべて Flask 公式ドキュメントが教えてくれた
  • Prototype 用、 Web アプリ用、 RESTful API 用の 3 パターンを考えたよ
  • GitHub にアップしたよ

github.com

動作環境

OS Version
Windows 10 Pro 1909
Application Version
PowerShell 5.1.18362.752
Language Version
Python 3.8.5
Package Version
Flask 1.1.2

前提条件

  • venv で仮想環境が構築済みで、 Activate された状態である
  • 仮想環境に Flask がインストール済みである

ユースケース毎のディレクトリ構成を考える

Flask アプリケーションはモジュールとして作るか、パッケージとして作るかでディレクトリ構成が大きく変わります。

プロトタイプ開発や小規模な Web アプリであれば、必要最小限の構成でもさほど煩雑にはならないため、単一モジュールに処理を詰め込んでもいいですが、ある程度スケールすることが見込まれる場合は複数の処理を意味のある単位に分割し、パッケージとして Flask アプリケーションを作るのが定石です。

msiz07-flask-docs-ja.readthedocs.io

個人的には、機能が複数 (例えば、検索・一覧表示機能と登録・更新機能など) になる場合は、初めからパッケージとしてモジュールを分割しておいた方が、後々のことを考えると無難だと思っています。

今回はユースケースに分けて、プロトタイプ開発や小規模 Web アプリケーションをターゲットするモジュール版、一定規模を想定したパッケージ版に加え、 SPA (Single Page Application) などからのアクセスに対応する RESTful API 版の 3 パターンを考えてみました。

プロトタイプ・小規模 Web アプリケーション

エントリーポイント (FLASK_APP 環境変数) をモジュールとして、ルーティングやビジネスロジックは一つの Python ファイルにまとめて記述します。

使い捨てとなるプロトタイプや単機能の Web アプリケーションなどを想定した、さっと手軽に作れるシンプルなディレクトリ構成です。

ディレクトリ構成

/
├ instance/
│ └ config/
│    ├ development.py
│    └ production.py
├ log/
├ static/
│ └ css/
│    └ default.css
├ templates/
│ ├ base.html
│ └ greeting.html
├ tests/
├ application.py
├ requirements.txt
└ settings.py

/instance/

Flask 0.8 から導入された Instance Folders (インスタンスフォルダ) 。

バージョン管理 (Git など) に含めない設定ファイルや非公開ファイル (API キーなど) を格納するディレクトリ。

Flask のインスタンスを生成する際、 instance_path パラメータや instance_relative_config パラメータでディレクトリの位置を設定できる。

msiz07-flask-docs-ja.readthedocs.io

/log/

Flask のログ出力先ディレクトリ。

ログ出力については、この記事では触れません。

/static/

JavaScriptCSS 、画像ファイルなどの静的ファイルを格納するディレクトリ。

msiz07-flask-docs-ja.readthedocs.io

/templates/

テンプレート (HTML) ファイルを格納するディレクトリ。

Flask はテンプレートエンジンとして Jinja を採用している。

msiz07-flask-docs-ja.readthedocs.io

/templates/base.html

<!DOCTYPE html>

<head lang="ja">
  <meta charset="utf-8" />
  <link rel="stylesheet" href="{{ url_for("static", filename="css/default.css") }}">
  <title>{% block title %}{% endblock %}</title>
</head>

<body>
  {% block content %}{% endblock %}
</body>

/templates/greeting.html

{% extends 'base.html' %}

{% block title %}flask-web-app{% endblock %}

{% block content %}
<h1>
  {{ greeting }} {% if user_name %}{{ user_name }}{% else %}World{% endif %} !!
</h1>
{% endblock %}

/tests/

UNIT テスト用ファイル格納ディレクトリ。

テストについては、この記事では触れません。

/application.py

エントリーポイントとして Flask インスタンスを生成する Python モジュール。

import os
from flask import Flask, request, render_template

app = Flask(__name__, instance_relative_config=True)

# 標準設定ファイル読み込み
app.config.from_object("settings")

# 非公開設定ファイル読み込み
if app.config["ENV"] == "development":
    app.config.from_pyfile(os.path.join("config", "development.py"), silent=True)
else:
    app.config.from_pyfile(os.path.join("config", "production.py"), silent=True)

@app.route("/", methods=("GET", "POST"))
@app.route("/<string:greeting>", methods=("GET", "POST"))
def greeting_user(greeting="Hello"):
    if request.method == "POST":
        user_name = request.form["user_name"]
    else:
        user_name = request.args.get("user_name", "")

    return render_template("greeting.html", greeting=greeting, user_name=user_name)

設定ファイル

標準設定

/settings.py

application.pyapp.config.from_object() メソッドから読み込まれる標準設定情報を記述。

バージョン管理に含まれるため、公開しても問題無い設定や後から上書きされるデフォルト設定のみを記述し、データベース接続情報や各種 API キーなどの非公開情報は記述しない。

非公開設定

/instance/config/development.py

/instance/config/production.py

application.pyapp.config.from_pyfile() メソッドから読み込まれる非公開設定情報を記述。

データベース接続情報や各種 API キー、 Flask の組み込みの設定値 SECRET_KEY もここへ記述する。

development.py は開発用の設定、 production.py には本番用の設定を記述し、 app.config["ENV"] の値によって設定を切り替える。

起動方法

PowerShell でアプリルートをカレントディレクトリとして flask run を実行すると、開発用サーバが起動します。

開発用サーバ起動前に、環境変数 FLASK_APP に Flask アプリケーション (インスタンス) が生成される Python モジュールを指定し、デバッグモードを有効にするために環境変数 FLASK_ENV には development を指定します。

PowerShell

> $env:FLASK_APP="application.py"
> $env FLASK_ENV="development"
> flask run

動作確認

Web ブラウザで http://127.0.0.1:5000/Hi?user_name=hrgm を開くと、起動した Flask アプリケーションへアクセスできます。

ちなみに、今回のサンプルは URL のパラメータを表示しているので、 Hihrgm を変更すると表示内容が変わります。

中規模 Web アプリケーション

エントリーポイントをパッケージとし、 Blueprint を用いてルーティングやビジネスロジックを別モジュールへ分割しています。

msiz07-flask-docs-ja.readthedocs.io

機能単位にモジュールを分割することで、今後の拡張によってコードが煩雑になることを防ぎます。

ディレクトリ構成

/
├ app/
│ ├ models/
│ ├ static/
│ │ └ css/
│ │    ├ default.css
│ │    └ style.css
│ ├ templates/
│ │ ├ base.html
│ │ └ greeting/
│ │    └ greeting.html
│ ├ views/
│ │ └ greeting.py
│ └ __init__.py
├ instance/
│ └ config/
│    ├ development.py
│    └ production.py
├ log/
├ tests/
├ MANIFEST.in
├ requirements.txt
├ settings.py
└ setup.py

※ 前述の プロトタイプ・小規模 Web アプリケーションのディレクトリ構成 との差分のみ記述します。

/app/

Flask アプリケーションのコードを含むディレクトリ。

機能別にモジュールを分割するため、 Python パッケージとする。

/app/models/

データベース接続や Model クラスを記述したファイルを格納するディレクトリ。

MTV (Model Template View) モデルの Model に相当するファイルを格納する。

データベース関連は、この記事では触れません。

/app/views/

ルーティングや、その URL に対するコールバック (ビジネスロジック) を記述したファイルを格納するディレクトリ。

MTV (Model Template View) モデルの View に相当するファイルを格納する。

/app/views/greeting.py

from flask import Blueprint, render_template, request

greeting = Blueprint("greeting", __name__, template_folder="templates")

@greeting.route("/", methods=("GET", "POST"))
@greeting.route("/<string:greeting>", methods=("GET", "POST"))
def greeting_user(greeting="Hello"):
    if request.method == "POST":
        user_name = request.form["user_name"]
    else:
        user_name = request.args.get("user_name", "")

    return render_template(
        "greeting/greeting.html", greeting=greeting, user_name=user_name
    )

/app/__init__.py

本来、 __init__.py には Python パッケージの初期化処理を記述しますが、特に Flask アプリケーションのエントリーポイントとなるパッケージでは、 Application Factory となる create_app() メソッドを定義します。

また、 Blueprint によって分割された views ディレクトリ配下の greeting モジュールから Blueprint オブジェクト (greeting) を import し、 Flask アプリケーションへ登録しています。

import os
from flask import Flask

def create_app(test_config=None):
    app = Flask(__name__, instance_relative_config=True)

    # 標準設定ファイル読み込み
    app.config.from_object("settings")

    # 非公開設定ファイル読み込み
    if app.config["ENV"] == "development":
        app.config.from_pyfile(os.path.join("config", "development.py"), silent=True)
    else:
        app.config.from_pyfile(os.path.join("config", "production.py"), silent=True)

    if test_config is not None:
        # テスト用設定を上書き
        app.config.from_mapping(test_config)

    from .views.greeting import greeting

    app.register_blueprint(greeting, url_prefix="/greeting")

    return app

/MANIFEST.in

当プロジェクトを配布・インストールする際に必要となるマニュフェストテンプレート。

/setup.py

当プロジェクトを配布・インストールする際に必要となる setup スクリプト

起動方法・動作確認

アプリルートで flask run を実行して開発用サーバが起動するのは前述の プロトタイプ・小規模 Web アプリケーションの起動方法 と同じです。

ただし、開発用サーバ起動前に設定する環境変数 FLASK_APP には、 Application Factory が定義されている Python パッケージを指定します。

PowerShell

> $env:FLASK_APP="app"
> $env FLASK_ENV="development"
> flask run

動作確認は前述の プロトタイプ・小規模 Web アプリケーションの動作確認 と同様です。

RESTful API

基本的には 中規模 Web アプリケーション と同様のディレクトリ構成ですが、レスポンスはすべて JSON 形式となるため、 static ディレクトリや templates ディレクトリは省いています。

ディレクトリ構成

/
├ api/
│ ├ models/
│ ├ views/
│ │ └ greeting.py
│ └ __init__.py
├ instance/
│ └ config/
│    ├ development.py
│    └ production.py
├ log/
├ tests/
├ MANIFEST.in
├ requirements.txt
├ settings.py
└ setup.py

※ 前述の プロトタイプ・小規模 Web アプリケーションのディレクトリ構成中規模 Web アプリケーションのディレクトリ構成 との差分のみ記述します。また、 Flask アプリケーションのディレクトリ名は api に変更しています。

/api/views/greeting.py

URL にバインドされたコールバック関数の戻り値を dict (辞書型) にすると、レスポンスの mimetypeapplication/json となり、 JSON データとして送信されます。

from flask import Blueprint, request

greeting = Blueprint("greeting", __name__)

@greeting.route("/", methods=("GET", "POST"))
@greeting.route("/<string:greeting>", methods=("GET", "POST"))
def greeting_user(greeting="Hello"):
    if request.method == "POST":
        user_name = request.form["user_name"]
    else:
        user_name = request.args.get("user_name", "")

    return {"greeting": greeting, "user_name": user_name}

起動方法・動作確認

前述の 中規模 Web アプリケーションの起動方法・動作確認 と同様。

GitHubリポジトリ

今回まとめた 3 パターンのディレクトリ構成は、 GitHubリポジトリを公開しています。

今後 Flask で開発をする際はプロジェクトテンプレートとして使用し、使いながらその都度、修正・改善をしていこうと思います。

github.com

おわりに

Flask アプリケーションのディレクトリ構成を考えるにあたり、様々なサイトを参考にさせていただきましたが、必要なことは Flask 公式ドキュメント にすべて書いてありました (当たり前) 。

僕は DjangoRails といった Web アプリケーションフレームワークをほんの少しかじった程度ですが、そういった大規模開発に対応できるフレームワークを模倣することで、 Flask の標準機能だけでもある程度の開発規模に耐えうるディレクトリ構成が実現可能だと思いました。

プロトタイプや小規模 Web アプリケーションの開発には手頃でちょうど良いと書きましたが、 Django よりも Flask の方が制約や規約が少なく自由度が高いため、機能数や画面数は少ないけれど複雑で特殊な処理が多いってケースには、多少規模が大きくなっても逆にフィットするかもしれません。

保守は大変になりそうですけど。。

いずれにしても適材適所なので、現在の Python の Web アプリケーションフレームワークである Django と Flask の二強体制は、開発規模によって使い分けるにはちょうど良い形なのかなと思っています。

が…!

Flask について調べた直後ではありますが、最近は巷で話題の FastAPI に興味津々です!!

fastapi.tiangolo.com

近いうちに使ってみたい ( º﹃º` )

*1:参照先は本家の Flask 公式ドキュメント (英語版) を個人の方が翻訳した日本語版。以降、 Flask 公式ドキュメントと記述されているものはすべて同様。