Edited at

【Pythonで多分人気2位のWebアプリケーションフレームワーク】Flaskの基本をわかりやすくまとめる


はじめに

PythonのWebアプリケーションフレームワークでDjangoと2トップの人気を誇るFlaskについてまとめます。

両者の比較とかはしません。というより私自身がDjango書いたことないので分かりません。。。

2018年4月よりバージョン1系が遂にリリースされました。今から始めるのにちょうどいい感じです。

前提知識としてPython文法の基礎知識が必要です。不安な方は自分の記事ですが、こちらの記事などをご確認ください。

私はこれまでFlaskを使ってAIを使って自分の顔がジャニーズ系かどうかを判定するWebサービスを作ってみたなどのアプリケーションを作ったことがあります。


Flask特徴


メリット


  • 覚えることが少ない

  • ドキュメントが豊富(英語)


デメリット


  • 大規模開発には向かない(マイクロフレームワークだから)

  • 日本語ドキュメントが不足(特にバージョン1系)


MVTモデル

Flaskはフレームワークの思想としてMVTモデルというものを採用しています。

一般的なMVCモデルとの比較は以下。


  • M(Model): MVCモデルのM(Model)相当。クラス定義などをしておく。

  • V(View): MVCモデルのC(Controller)相当。URLリクエストを振り分ける。MVCのV相当でないため混乱しやすい。

  • T(Template): MVCモデルのV(View)相当。画面表示を担う。


インストール

公式サイトを参考ください。

煽り抜きで難しくないです。


Hello World

リクエストしたら"Hello, World"の文字列をレスポンスとして返す最小構成のプログラムを例とします。

ファイル名は基本的になんでもよいですが、"flask.py"だけは名前重複でエラーになるためNGだそうです。

run関数で起動します。run関数の引数については以下の通りです。


  • hostキーワード引数: サーバのIPアドレスを指定する。

  • portキーワード引数: ポート番号を指定する。指定しなければ、デフォルトではポート番号は5000。

  • debugキーワード引数: デバッグモードを有効にするかどうかを指定する。開発環境では有効にするが、production環境では無効にする。指定しなければ、デフォルトでは無効。


hello.py

from flask import Flask # Flaskパッケージを要インポート


app = Flask(__name__) # Flaskクラスのインスタンス生成

@app.route('/') # URL指定。URLにリクエストが来ると関数内が実行される。
def index():
return 'Hello, World!'

if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)



起動ログ

$ python hello.py

* Serving Flask app "hello" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 329-124-557


Routing

RoutingはMVTモデルのViewに相当する処理です。

関数の直前にrouteデコレータを定義し、引数にURLとHTTPメソッドタイプを設定します。

routeデコレータの引数については以下です。


  • 第1引数: URL指定する。

  • methodsキーワード引数: メソッドタイプを指定。指定しない場合はGETメソッドになる。

※ここではIPアドレスを127.0.0.0.1、ポート番号を8080とする。


URLパラメータなし

"http://127.0.0.1:8080/"

にGETメソッドでアクセスした場合は以下の関数が実行されます。GETメソッドの場合はmethodsキーワード引数は省略可能です。

@app.route('/')

def index():
return 'Index Page'

"http://127.0.0.1:8080/hello"

にPOSTメソッドでアクセスした場合は以下の関数が実行されます。

@app.route('/hello', methods=['POST'])

def hello():
return 'Hello, World'


URLパラメータあり

"http://127.0.0.1:8080/user/(username)"

にGETメソッドでアクセスした場合は以下の関数が実行されます。

(username)はURLに組み込んだリクエストパラメータ。デフォルトではこのパラメータは文字列として扱われます。

パラメータの取得はパラメータをそのまま関数の引数に与えればそのまま変数として使えます。

@app.route('/user/<username>')

def show_user_profile(username):
return 'User %s' % username

"http://127.0.0.1:8080/sample/(int:post_id)"

にGETメソッドでアクセスした場合は以下の関数が実行されます。

パラメータの型は変換することができ、例えば、"int:"をつけることでリクエストパラメータは数字として扱われます。

他にはstring, float, path, uuidが用意されている。

@app.route('/sample/<int:id>')

def sample(id):
return 'ID: %d' % id


requestモジュール

flaskパッケージのrequestモジュールを使用して、外部からのメソッドタイプ指定やリクエストパラメータなどの値を取得できます。


  • request.method: リクエスト元(HTMLのformタグのmethod属性など)が指定したメソッドタイプを取得

  • request.form['formのname値']: HTMLのformタグで送信されるリクエストパラメータを取得

  • request.files['formのname値']: HTMLのformタグで送信されるファイルを取得

from flask import Flask, request #requestモジュールを要インポート


app = Flask(__name__)

@app.route('/method', methods=['GET', 'POST'])
def sample_method():
if request.method == 'POST':
pass
else if request.method == 'GET':
pass

@app.route('/title')
def sample_title():
title = request.form['title']
pass

@app.route('/image', methods=['POST'])
def sample_image():
request_img = request.files['image']
pass

if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)


Rendering Templates

Flaskはjinja2テンプレート連携が容易に行えます。


jinja2テンプレート

jinja2はPythonのテンプレートエンジンです。

jinja2テンプレートを利用することでHTML内で変数を参照したり制御構文を記載できたりします。ちなみに、jinjaは日本の「神社」が名前の由来らしいです。


変数取得


nameという変数を取得する例

{{ name }}



制御構文


if文

{% if condition1 %}

Condition1 is true
{% elif condition2 %}
Condition2 is true
{% else %}
Default logic goes here
{% endif %}


for文

{% for book in books %}

<tr>
<td>{{book.author}}</td>
<td>{{book.rating}}</td>
</tr>
{% endfor %}


コメントアウト

{# comment out here #}


template継承

各ページで共通する部分は1つのファイルに記述してそれを継承するようにできます。


共通部分

一般的にはヘッダーやフッターなどを共通部分として定義します。

{% block content %}と{% endblock %}の間に共通部分を呼び出す元の個別部分が入ります。


common.html

<!doctype html>

<html>
<head>
<title>{{ title }}</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>


個別部分

共通部分を{% extends %}で継承します。あとは、{% block content %}と{% endblock %}の間に各ページの個別部分を記載していきます。


hello.html

{% extends 'common.html' %}

{% block content %}
<h1>Hello</h1>
{% endblock %}


render_template関数

render_template関数でjinja2テンプレートを返します。

jinja2テンプレートへ変数を渡す際には、キーワード引数で指定します。変数は数字や文字列などの他にリスト形式で送信することも可能です。

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
shop = 'A食堂'
foods = []
foods.append({'name':'ラーメン', 'price':700})
foods.append({'name':'カレー', 'price':500})
foods.append({'name':'かつ丼', 'price':800})
return render_template('sample.html', shop=shop, foods=foods)

if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)


sample.html

<!doctype html>

<html>
<head>
<title>sample</title>
</head>
<body>
ようこそ、{{ shop }}です。
<br>
<ul>
{% for food in foods %}
<li>{{ loop.index }}: {{ food.name }} - {{ food.price }}円</li>
{% endfor %}
</ul>
</body>
</html>


リダイレクト

Flaskではredirect関数でリダイレクトします。

下の例では、/にリクエストすると/loginにリダイレクトされます。

また、下記の例で使用されているurl_for関数は、URLを生成する関数です。URL文字列を全て記載する必要がなくなるため、ルートURLに変更があった際でも変更箇所が少なくてすむメリットがあります。

from flask import abort, redirect, url_for #redirectモジュール要import


@app.route('/')
def index():
return redirect(url_for('login'))

@app.route('/login')
def login():
reuturn 'Redirected!'


Session

flaskのsessionでkey-value形式によりセッション情報を管理します。

「セッション」とはサーバに持たせる機密情報(ログインID/パスワードなど)のことです。


  • 取得:session['key']

  • 設定:session['key'] = value

  • 削除:session.pop('key', None)

また、flaskのsessionを利用するにはsecret_keyが必要です。これが無いと"RuntimeError: The session is unavailable because no secret key was set. Set the secret_key on the application to something unique and secret."とエラーになります。

from flask import Flask, session, redirect, url_for, escape, request # sessionモジュールを要インポート


app = Flask(__name__)

app.secret_key = b'XXXXXXXXX' # sessionを利用するために必要

@app.route('/')
def index():
if 'username' in session:
return 'Logged in as %s' % escape(session['username']) # セッション値を取得
return 'You are not logged in'

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username'] # セッション値を設定
return redirect(url_for('index'))
return '''
<form method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''

@app.route('/logout')
def logout():
session.pop('username', None) # セッション値を削除
return redirect(url_for('index'))


Cookie

flaskのcookieでkey-value形式によりクッキー情報を管理します。

「クッキー」とはブラウザに持たせる情報(セッションIDなど)です。


  • 取得:request.cookies.get('key')

  • 設定:response.set_cookie('key', 'value')

from flask import request, make_response # requestモジュールを要インポート


@app.route('/getCookie')
def get_cookie():
user_id = request.cookies.get('user_id')# Cookie取得
return user_id

@app.route('/setCookie')
def set_cookie():
content = 'This is a sample response content'
response = make_response(content)
response.set_cookie('user_id', '1234')# Cookie設定
return response


エラーハンドラ

errorhandlerによりエラー時の動作を自分で定義できます。

例えば、404エラー時に自作HTMLファイルを画面に出力したい場合は以下のように実装します。

from flask import render_template

@app.errorhandler(404)
def page_not_found(error):
return render_template('page_not_found.html', 404)


マルチスレッド

Flaskのデフォルトでは複数のリクエストを同時に処理することができません。同時アクセスがあると最初のリクエストの処理が完了するまで待ちが発生する仕様になっています。

マルチスレッドを有効にするには、app.run関数に"threaded=True"パラメータを渡すだけです。

ただし、これはuWSGI+Nginxなどのミドルウェアを使用する場合では不要の設定であり、あくまで開発環境用に必要であれば設定するようなものです。

app.run(host='127.0.0.1', port=8080, threaded=True)


HTTPS

app.run関数のssl_contextキーワード引数にsslインスタンス(証明書と鍵をロード済みのもの)を渡せばHTTPSで起動します。

前提としてSSL証明書と鍵は別途用意が必要です。


  • SSLインスタンス生成:ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)

  • 証明書と鍵のロード:load_cert_chain('cert', 'key')

from flask import Flask, render_template, request

import ssl # ssl要インポート

app = Flask(__name__)

context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) # SSLインスタンス生成
context.load_cert_chain('cert.crt', 'server_secret.key') # 証明書と鍵のload

@app.route("/")
def index():
return render_template('index.html')

if __name__ == "__main__":
app.run(host='0.0.0.0', port=443, ssl_context=context, debug=True)


ディレクトリ構成

それほど厳密にディレクトリ構成が決まっているわけではないですが、ベストプラクティス的なものが世の中にありますので、まとめてみました。

以下に記載する規模感は個人の感覚によるところが大きいですが、趣味でアプリを一人で作ってみる程度なら小規模の構成でも充分だと思います。

(そもそもPythonファイル1つでWEBアプリ作れちゃうのがFlaskの1つの強みだったりします。)複数人でプロトタイプ作りますとかだと中規模の構成を採用すべきかなと思います。


小規模(1ファイルにまとめる)

1つのファイル(ここではyourapplication.py)にまとめてコーディングするような場合はこのようなディレクトリ構成にします。

yourapplication/

requirements.txt # 依存パッケージ
config.ini # config
instance/
config.ini # バージョン管理対象外にしたいConfig。認証情報とかデバッグモードなどを記載。
yourapplication.py # サーバーサイド処理を記載するファイル
static/
css/ # CSSを格納するディレクトリ
js/ # JavaScriptを格納するディレクトリ
images/ # 画像を格納するディレクトリ
templates/ # HTML(jinja2)を格納するディレクトリ


小規模(ファイル分割)

さすがに一つのファイルに全てを書くのはきついなーってなってきたらyourapplication.pyをmodels.py(MVTのM)とviews.py(MVTのV)に分割してこのようなディレクトリ構成にします。

また、models.pyとviews.pyはそれぞれ1つのファイルでなくmodelsディレクトリとviewsディレクトリを用意して各ディレクトリ内でさらにファイルを分割してもよいです。

yourapplication/

requirements.txt
config.ini
instance/
config.ini
run.py # 開発環境用の実行ファイル。プロダクション環境では使わない。
yourapplication/
views.py # routeを定義する。models.pyで定義したものを呼び出し、リクエストURL毎の処理の振り分け、リクエストパラメータ取得、レンダーテンプレート、リダイレクトなどをする。
models.py # モデル(関数、クラス、フィールド、メソッドなど)を定義する。
static/
css/
js/
images/
templates/

models.pyとviews.pyとrun.pyの簡単な例は以下です。

models.pyで関数を定義し、views.pyで関数を呼び出し、run.pyで実行する。


models.py

def add(a, b):

return a + b


views.py

import models.add

@app.route('/')
def index():
return 'Index Page'

@app.route('/add')
def add_1_1():
return add(1, 1)



run.py

from yourapplication import app

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



中規模

中規模ならBlueprintsを使ってファイルを分割します。


Blueprint

Blueprintは肥大化しがちなView(views.pyファイル)を分割する方法です。

※ここではtemplatesとstaticの分割までは触れません。


Blueprint前の例


ディレクトリ構成

yourapplication/

yourapplication.py
static/
templates/
func1_a.html
func2_a.html


yourapplication.py

from flask import Flask

app = Flask(__name__)

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

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

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



Blueprint後の例


ディレクトリ構成

yourapplication/

app.py # 分割したファイルを統合するファイル
func1.py
func2.py
static/
templates/
func1_a.html
func2_a.html

app.pyでは分割した各Blueprintインスタンスをregister_blueprint関数で統合します。


app.py

from flask import Flask

from yourapplication.func1 import func1
from yourapplication.func2 import func2

app = Flask(__name__)

# 統合
app.register_blueprint(func1)
app.register_blueprint(func2)

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


分割したファイル側では、Blueprintクラスのインスタンスを生成します。


  • url_prefixキーワード引数: URL指定。例えば、下記ではfunc1/aにリクエストするとfunc1_a関数が実行されます。

  • template_folderキーワード引数: 探索するtemplateのディレクリパス指定

  • static_folderキーワード引数: 探索するstaticのディレクリパス指定


func1.py

from flask import Blueprint, render_template

# func1のBlueprintインスタンス生成
func1 = Blueprint('func1', __name__, url_prefix='/func1', template_folder='templates', static_folder='static')

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



func2.py

from flask import Blueprint, render_template

# func2のBlueprintインスタンス生成
func2 = Blueprint('func2', __name__, url_prefix='/func2', template_folder='templates', static_folder='static')

@func2.route('/a')
def func2_a():
return render_template('func2_a.html')



Blueprintを使ったディレクトリ構成

結局のところ、Blueprint使ったらどんなディレクトリ構成にするべきかはこんな感じでしょうか。ただし、これは賛否両論ありそうです。機能ごとにtemplatesディレクトリとstaticディレクトリを用意するべきとか。

yourapplication/

requirements.txt
config.py
instance/
config.py
yourapplication/
app.py
models.py
static/
css/
js/
images/
templates/
func1.py
func2.py


大規模

PythonならDjango使おう!(笑)


Nginx + uWSGI

production環境ではアプリケーションを組み込みサーバ上で動作させずに、ミドルウェア(一般的なのはNginx+uWSGIの組み合わせ)をかませることが公式で推奨されています。

NginxはオープンソースのWebサーバです。消費メモリが少なくリバースプロキシ機能やロードバランサ機能を持つのが特徴で、最近ではApacheよりもNginxを使うのが一般的になってきました。(両者の長所短所はもちろんあります。)

uWSGIはアプリケーションとWebサーバをつなぐ役割を担います。

image.png

ここではNginxとuWSGIに関する細かい説明には立ち入らずにミニマムな設定内容と起動方法だけ記載します。

もっと本格的な運用をする場合はこちらの記事を参考にするとよさそう。

また、過去に私はこのような設定をしたこともあります。


Nginx設定


/etc/nginx/conf.d/sample.conf

server {

listen 80;
location / {
include uwsgi_params;
uwsgi_pass unix:///tmp/uwsgi.sock;
}
}


uWSGI設定


myapp.ini

[uwsgi]

module = app
callable = app
master = true
processes = 1
socket = /tmp/uwsgi.sock
chmod-socket = 666
vacuum = true
die-on-term = true


起動

上記Nginxのconfig反映のためにNginxを再起動。

上記iniファイルを読み込んでuWSGIを起動。

$ systemctl restart nginx.service

$ uwsgi --ini myapp.ini