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

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

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

はじめに

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

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

この記事では、エラーハンドリング について紹介していきます。色々書いていたら長くなってしまいました。

FlaskのError Handling

アプリケーションで発生した例外を処理するための error handler を登録します。error handlerは例外と関数を組み合わせて登録します。例外発生時に、その例外のエラーハンドラーが登録されているか参照し、登録されている場合に対応する関数が呼び出されます。

Application Errors — Flask Documentation (0.12)

error handlerの登録

以下のいずれかの方法でエラーハンドラを登録できます。

  • errorhandlerデコレータ
  • register_error_handler関数

errorhandlerデコレータ

Flaskのアプリケーションオブジェクト、もしくは、Blueprintの関数として用意されています。errorhandler()の引数に例外を指定し、対象の関数をデコレートします。

app = Flask(__name__)
app.errorhandler(Exception)
def exception_handler(e):
    return "handling exception"

from werkzeug.exceptions import NotFound
bp = Blueprint('exception', __name__)
bp.errorhandler(NotFound)
def bp_notfound(e):
    return "handling NotFound"

これで、Exception発生時にexception_handler(e)、NotFound発生時にbp_notfound(e)がそれぞれ実行されるようになります。

errorhandlerに登録された例外だけでなく、サブクラスの例外が送出された場合も関数が実行されます。上記の例ですと、Pythonの例外はすべてExceptionのサブクラスであるため、どんな例外が発生しても、exception_handler(e)が実行されます。サブクラスがエラーハンドラーに登録されている場合は、そちらが優先されます。

サブクラスを補足する挙動は非常に便利ですが、Exceptionのように無闇に設定してしまうと重要な例外を握りつぶしてしまう可能性があります。どのような例外が発生する可能性があるか把握して、1つ1つ設定したほうが良いと思います。

register_error_handler関数

errorhandler()の代わりに、register_error_handler()で例外と関数を登録できます。Blueprintも同様です。

from werkzeug.exceptions import NotFound
app = Flask(__name__)
def app_notfound(e):
    return "handling NotFound"

app.register_error_handler(NotFound, app_notfound)

引数は関数なので、当然ラムダ式を埋め込むこともできます。

app.register_error_handler(NotFound, lambda e: render_template('notfound.html'))

1行で済む処理の場合は、ラムダ式の方がスッキリするかもしれません。たとえば上記のようにrender_template()のみの場合などです。

例外の送出

例外を発生させる方法は、raiseabort があります。raisePython標準のもので、abortはflaskからインポートしていますが、その実体はwerkzeug.exceptions の関数です。

from flask import abort
from werkzeug.exceptions import NotFound
app = Flask(__name__)
app.errorhandler(NotFound)
def app_notfound(e):
    return "handling NotFound"

app.route('/raise/notfound')
def raise_notfound():
    raise NotFound

app.route('/abort/notfound')
def abort_notfound():
    abort(404)

raiseはExceptionのサブクラスを指定し、abortステータスコードも指定できます。

abortは内部で、werkzeug.exceptions.HTTPExceptionのサブクラスの例外をraiseしています。つまり、どちらの手段を使っても最終的にはraiseで例外を発生させています。abortであれば、ステータスコードのみ指定すればよいので各種例外のインポートが不要です。errorhandlerにも同様にステータスコードによる指定が可能です。

app.errorhandler(404)
def app_notfound(e):
    return "handling NotFound"

HTTPExceptionのサブクラスの例外が発生した場合のみ、ステータスコードによる参照を行い、それ以外の場合は例外の型による参照を行います。

werkzeug.exceptions.NotFoundのようなHTTPExceptionのサブクラスはいずれも内部にステータスコードを持っており、エラーハンドラはこのステータスコードを参照しています。なので、HTTPExceptionのサブクラスに関しては、ステータスコードさえ合っていればよいです。

したがって、下記のように例外クラスを発生させても、errorhandler(404)でデコレートされた関数が実行されます。

app.errorhandler(404)
def app_notfound(e):
    return "handling NotFound"

app.route('/raise/notfound')
def raise_notfound():
    raise NotFound

ステータスコードでも例外クラスでも統一されていればどちらでもよいと思います。個人的には、ステータスコードだけでさくっと書いてしまう方が好きです。

Blueprintのエラーハンドリングの対象について

Blueprint.errorhandler()の場合、ハンドリングの対象はそのBlueprintに閉じます。したがって、以下のいずれのURLにアクセスしてもbp_notfound_errorhandler(e)は実行されません。

bp = Blueprint('bp', __name__)
bp.errorhandler(404)
def bp_notfound_errorhandler(e):
    return 'no calling this function'

app = Flask(__name__)
app.route('/raise/notfound')
def raise_notfound():
    abort(404) 

another_bp = Blueprint('another_bp', __name__)
another_bp.route('/bp/raise/notfound')
def another_bp_raise_notfound(e):
    abort(404)

一方で、以下の場合はnotfound_errorhandler(e)が実行されます。Flaskのアプリケーションオブジェクトに登録されたエラーハンドラは、Blueprintからも見えます。

app = Flask(__name__)
app.errorhandler(404)
def notfound_errorhandler(e):
    return 'calling this function'

bp = Blueprint('bp', __name__)
bp.route('/bp/raise/notfound')
def bp_raise_notfound():
    abort(404) 

仮に、FlaskのアプリケーションオブジェクトとBlueprintに同じ例外に対するエラーハンドラが登録されており、そのBlueprintで例外が発生した場合、Blueprintのエラーハンドラが優先されます。

なお、Routingに存在しないURLにアクセスされた場合、FlaskはNotFoundの例外を発生させます。例外を検知して専用のエラーページを返したい場合は、Flaskアプリケーションオブジェクトにerrorhandler(404)を登録する必要があります。

Custom Error Pages — Flask Documentation (0.12)

HTTPExceptionのサブクラスの例外発生時の挙動

errorhandlerに登録した例外のサブクラスである例外が発生した場合にも関数が実行されると述べました。HTTPExceptionのサブクラスの場合には少し事情が異なります。

NotFoundBadRequestなどのHTTPExceptionのサブクラスをすべて検知しようと以下のようなソースコードを書いても機能しません。

from werkzeug.exceptions import HTTPException

app = Flask(__name__)
app.errorhandler(HTTPException)
def http_exception(e):
    return "occured http exception"

機能しない理由は、前述した「HTTPExceptionのサブクラスはステータスコードを参照する挙動」にあります。

エラーハンドラの登録形式

register_error_handler もしくは errorhandler で登録されたハンドラーの一覧は、error_handler_spec という変数に格納されています。いくつか例外を登録し、どのようなに格納されているか見てみましょう。

app.register_error_handler(HTTPException, lambda: None)                         
app.register_error_handler(NotFound, lambda: None)                              
app.register_error_handler(400, lambda: None) 
app.register_error_handler(Exception, lambda: None)    
{None:{
    None: {
        <class 'Exception'>: <function <lambda> at 0x1063eb378>,
        <class 'werkzeug.exceptions.HTTPException'>: <function <lambda> at 0x105438048
    },
    400: {<class 'werkzeug.exceptions.BadRequest'>: <function <lambda> at 0x1063eb2f0>},
    404: {<class 'werkzeug.exceptions.NotFound'>: <function <lambda> at 0x1063eb158>}
}}

ステータスコードを持つHTTPExceptionのサブクラスは、そのステータスコードをキーとして登録されます。一方、Exceptionのようにステータスコードを持たない例外はNoneをキーとして登録されています。同様にHTTPExceptionもステータスコードを持たないため、Exceptionと同じくNoneをキーとして登録されています。このNoneで登録されてしまうのがhttp_exception(e)が実行されない理由です。

具体例

HTTPExceptionのサブクラスの例外は、ステータスコードと合致するキーがある場合にエラーハンドラを返します。キーがなければエラーハンドラは返されません。

例えば、この状態でabort(401)が実行された場合、raise Unauthorizedが内部で実行されます。UnauthorizedはHTTPExceptionのサブクラスですが、401のステータスコードを持ちます。しかし、error_handler_specには、401のキーが存在しないためエラーハンドラは返されません。

キーの意味

ステータスコードの有無を表すキー以外に、先頭にNoneというキーがあります。 この先頭のNoneは、Flaskのアプリケーションオブジェクトに登録されたエラーハンドラであることを示しています。Blueprintの場合は、そのBlueprintの名前をキーとして、エラーハンドラが格納されます。

app = Flask(__name__)
app.register_error_handler(400, lambda: None) 
bp = Blueprint('bp', __name__)
bp.register_error_handler(404, lambda: None) 
{
    None: {
        400: {<class 'werkzeug.exceptions.BadRequest'>: <function <lambda> at 0x103953048>}
    },
    'bp': {
        404: {<class 'werkzeug.exceptions.NotFound'>: <function <lambda> at 0x104907158>}
    }
}

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

はじめに

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

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

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

Blueprints?

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

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

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

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'
2. 実装したBlueprintをFlaskのアプリケーションに登録する

続いて、作成した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/views.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/views.py
func3 = Blueprint('func3', __name__, url_prefix='func3', static_folder='./static')
/FlaskAppDirectory
├── app.py
├── static
└── func3
    ├── __init__.py
    ├── static
    │   └── test.css
    ├── templates
    └── views.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/views.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
    └── views.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'

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

はじめに

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

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

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

Logging

Flaskのloggerを使うことで、Python標準のloggingLoggerを取得できます。パッケージ名を名前に持つLoggerが取得されます。

# log_test.py
from flask import Flask                                                         
                                                                                  
app = Flask(__name__)                                                                                                                          
                                                                            
@app.route('/')                                                                 
def index():                                                                    
    app.logger.debug('debug')                                                   
    app.logger.info('info')                                                     
    app.logger.warn('warn')                                                     
    app.logger.error('error')                                                   
    app.logger.critical('critical')                                                                                                                         
    return "logging"                                                            
                                                                                                                                                    
if __name__ == '__main__':                                                      
    app.run(debug=True)              

上記アプリケーションにアクセスすると、コンソールに各種ログが出力されていることが確認できると思います。

デフォルトで登録されているHandler

app.loggerには、DebugHandlerProductionHandler が登録されています。debug=True(デバッグモード)で実行した場合はDebugHandlerで、そうでない場合はProductionHandlerでログが出力されます。それぞれのHandlerは異なるログレベルとログフォーマットが設定されています。

つまり、デバッグモードで実行するか否かでログレベルとログフォーマットが変化します。ログレベルについてはこちらを参照ください。

デバッグモードで実行した場合

DEBUG以上 のログレベルのログが出力されます。また、このときのログフォーマットは以下になります。

--------------------------------------------------------------------------------
%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:
%(message)s
--------------------------------------------------------------------------------
デバッグモードでない場合

ERROR以上 のログレベルのログが出力されます。ログのフォーマットは以下になります。

[%(asctime)s] %(levelname)s in %(module)s: %(message)s

ログの出力先を変更する

Python標準のLoggerは、Handler によって出力先を変更します。そして先述した通り、app.loggerで得られるLoggerはPython標準のものです。つまり、Flaskアプリケーションでも同様に、LoggerにHandlerを追加/削除することでログの出力先を変更できます。

Pythonには様々なHandlerが標準で用意されています。これらのHandlerを追加すれば良いです。ちなみに、コンソールに出力しているHandlerは StreamHandler です。

RotatingFileHandler

RotatingFileHandler は、ログをファイルに出力し、かつ、サイズに応じてローテーションしてくれる便利なHandlerです。これを例にFlaskのLoggerにHandlerを追加します。

import logging                                                                  
import logging.handlers  
from flask import Flask                                                      
                                                                                  
app = Flask(__name__)     

# Add RotatingFileHandler to Flask Logger
handler = logging.handlers.RotatingFileHandler("test.log", "a+", maxBytes=3000, backupCount=5)
handler.setLevel(logging.INFO) 
handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s in %(module)s: %(message)s'))
app.logger.addHandler(handler)

@app.route('/')                                                                 
def index():                                                                    
    app.logger.debug('debug')                                                   
    app.logger.info('info')                                                     
    app.logger.warn('warn')                                                     
    app.logger.error('error')                                                   
    app.logger.critical('critical')                                                                                                                         
    return "logging"                                                            
                                                                                                                                                    
if __name__ == '__main__':                                                      
    app.run(debug=True)                                 

同様にアプリケーションにアクセスすると、test.logが生成され、ログメッセージが書かれていることが確認できます。 このようにして、Python標準のLoggerと同じようにHandlerを追加できます。

Python標準のLoggerのみを使いたい場合

FlaskのLoggerを使わず、それらを無効にしたい場合もあるかもしれません。そのときの設定方法です。アプリケーションのできるだけ早い段階で無効化処理をしておかないと、思わぬところでログが出力されてしまう可能性があります。

import logging                                                                  
import logging.handlers  
from flask import Flask                                                         
                                                                                  
app = Flask(__name__)   
# FlaskのLoggerを無効化する
app.logger.disabled = True  
# werkzeugのLoggerを無効化する
werkzeug_logger = logging.getLogger('werkzeug')   
werkzeug_logger.disabled = True  

また、FlaskのLoggerにデフォルトで登録されているHandlerを削除したい場合は以下です。

app.logger.handlers.clear()

ただ、すべてのHandlerを削除するくらいなら、上記の方法で無効化してPython標準のLoggerにHandlerを登録したほうがわかりやすくて良い気がします。

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

はじめに

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

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

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

Template Engine?

Webアプリケーションでは、「画面のレイアウトは同じだが、その画面の一部のデータは異なる表示にしたい」ということがよくあります。たとえば、ユーザ情報画面です。ユーザ情報画面では、ログインしているユーザごとに名前やIDを表示するのが普通です。つまり、データや状態によって画面を動的に生成する必要があります。これを実現しているのがテンプレートエンジンです。

Flaskのデフォルトのテンプレートエンジンは、 jinja2 です。jinja2 では、以下のように HTML と Python のコードを同居させることができます。この user.html を Flask で処理して返すことで、1つのHTMLファイルで異なる画面を表示できます。

<!-- user.html -->
<p>
    {{ user_name }}
</p>

<p></p> がHTMLのタグで、 {{...}} で囲われた部分がPythonのコードです。{{...}}のようなPythonの値を埋め込むための記号を Directive と呼びます。値をHTMLに出力するには、{{...}}を使い、if文などの式を埋め込みたい場合には、{%...%}など、目的別にディレクティブを利用します。詳しくは、jinja2の公式ドキュメントを参照していただければと思います。

Template

render_template() 関数を利用することで、Template(=雛形) である user.html に サーバのデータを組み込めます。

from flask import Flask
from flask import session
from flask import render_template

app = Flask(__name__)

@app.route('/user')
def user():
    user_name = 'my name'
    return render_template('user.html', user_name=user_name)

わかりやすくするために、user_name はセッションからの取得ではなく、固定値を与えています。上記が実行されると、以下のHTMLが生成されます。

<!-- rendered user.html -->
<p>
    my name
</p>

Escape

Flaskでは {{ }} で出力される値は、自動的にエスケープされます。

@app.route('/user')
def user():
    user_name = '<script>alert("alert")</script>'
    return render_template('user.html', user_name=user_name)

値はエスケープされるので、以下のようなHTMLが出力されます。scriptタグの<>がそれぞれエスケープされていることがわかります。したがってXSSの心配はありません。

<!-- rendered user.html -->
<p>
    <!-- エスケープされているのでスクリプトは実行されない -->
    &lt;script&gt;alert(&#39;alert&#39;)&lt;/script&gt;
</p>

一方で、エスケープしたくないケースもあるかと思います。その場合は、{% autoescape %} を用います。ただし、ユーザの入力値を表示する場合はXSSの危険性があるので、使い所はしっかり検討しなければなりません。

<!-- user.html -->
<p>
    {% autoescape False %}
        {{ user_name }}
    {% endautoescape %}
</p>
<!-- rendered user.html -->
<p>
    <!-- scriptタグのJavaScriptが実行される -->
    <script>alert("alert")</script>
</p>

共通化

上記のようなテンプレートを作っていると、多くの画面で共通している要素や処理が出てくることがあります。それらをうまく共通化するための機能として、 extendsmacros があります。これらはいずれもFlaskではなくjinja2の機能ですが、便利な機能なのでここで紹介しておきます。

extends

テンプレートを継承することで、親となるテンプレートに子となるテンプレートの要素を加えた画面を生成できます。

<!-- parent.html -->
<p> parent content </p>
{% block body %}{% endblock %}
<p> parent end </p>
<!-- child.html -->
<p> not contain part </p>
{% extends "parent.html" %}
{% block body %}
    <p> this is child content </p>
{% endblock %}

{% extends %} で、親のテンプレートを指定します。子のテンプレートでは、{% block 識別子 %} で子の内容を定義します。識別子は、親と子で揃えておく必要があります。揃っていれば、識別子は自由につけることができます。ここでは識別子は、body としています。

上記は以下のようなHTMLとして生成されます。{% block %} に含まれていない部分は生成結果にも含まれません。

<p> parent content </p>
    <p> this is child content </p>
<p> parent end </p>

ナビゲーションバーやフッターなど多くの画面に共通するコンポーネントを親テンプレートとして作成することで、各画面に同じ要素を記述する必要がなくなります。

macros

extends よりももう少し細かい、特定の処理や部品を再利用できるようにするための機能が macros です。以下のようにして、macroを定義できます。macroはPythonの関数のように引数を指定できます。

<!-- macro.html -->
{% macro generate_h5(text)) %}
<h5>{{ text }}</h5>
{% endmacro %}

定義したmacroは他のテンプレートから以下のようにして呼び出せます。macroが別のファイルに記載されているのであれば、macroが定義されているファイルからimportする必要があります。

<!-- using_macro.html -->
{% from "macro.html" import generate_h5 %}
{{ generate_h5('one') }}

このテンプレートは以下のようなHTMLにレンダリングされます。

<h5>one</h5>

通常、macroの呼び出し先で呼び出し元の変数を参照できません。import時にwith context とつけると、呼び出し元と呼び出し先で変数コンテキストが共有されます。したがって、macro呼び出し時に変数の受け渡しが不要となります。macro呼び出し元のhtmlで引数を指定していませんが、レンダリング結果は同じです。

<!-- macro.html -->
{% macro generate_h5()) %}
<h5>{{ text }}</h5>
{% endmacro %}
<!-- using_macro_with_context.html -->
{% set text = 'one' %}

{% from "macro.html" import generate_h5 with context %}
{{ generate_h5() }}

状況にもよりけりですが、無闇にwith contextを使うと、macroとmacroの呼び出し元の依存が強くなってしまうように感じます。一瞬便利のような気がしてバシバシ使いたくなりますが、よく吟味したほうが良いと思っています。

Standard Context

Flaskでは、テンプレート内でいくつかの変数や関数がデフォルトで利用可能です。すべての利用可能な変数は、公式ドキュメント に記載されています。sessionurl_for()など、知っておくと便利なものもあるので事前に確認しておくと幸せになれそうです。

Customize Directive

jinja2のディレクティブをFlask経由で変更できます。以下の例では、{{...}} {%...%} {#...#}をそれぞれ<<...>> <%...%> <#...#>に変更しています。

from flask import Flask

app = Flask(__name__)

jinja_options = app.jinja_options.copy()                                         
jinja_options.update({                                                      
    'block_start_string': '<%',                                                 
    'block_end_string': '%>',                                                   
    'variable_start_string': '<<',                                              
    'variable_end_string': '>>',                                                
    'comment_start_string': '<#',                                               
    'comment_end_string': '#>'                  
})                                                                               
app.jinja_options = jinja_options    

app.jinja_options.update()でオプションを直接追加しようとすると、TypeError: 'ImmutableDict' objects are immutableが発生してしまい変更できません。そのため、一度コピーを取り、そのコピーに値を追加しています。

他のライブラリなどとディレクティブが競合した際に、この設定が有用です。具体的には、 Vue.js などが例として挙げられます。

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

はじめに

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

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

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

Routing?

ルーティングとは、URLと処理を対応づけることです。Flaskのルーティングは、URLとfuncitonを紐付けます。ルーティングには、route() を用います。

from flask import Flask
app = Flask(__name__)

@app.route('/') # http://xxx 以降のURLパスを '/' と指定
def index():
    return 'Index Page'

上記の例では、 http://xxx/ と index() を紐付けています。http://xxx/ にアクセスすると、「Index Page」とだけ書かれた質素なページが表示されます。これがもっともシンプルなルーティングです。

この記事ではそのほか、以下のルーティングについて触れます。

  • HTTPメソッドに応じたルーティング
  • 変数付きのルーティング
  • 静的ファイルのルーティング

HTTPメソッドに応じたルーティング

route() は、HTTPメソッド名のリストを渡してやることで、そのURLが受け入れるHTTPメソッドを指定できます。

@app.route('/edit', methods=['GET', 'POST'])
def edit():
    # 共通の処理
    if request.method == 'get':
        # GET時の処理
    else:
        # POST時の処理
    # 共通の処理

methods に値を指定することで、HTTPメソッドを指定できます。例では、GETとPOSTのリクエストをルーティングしています。

HTTPメソッドの指定を変えることで、同じURLでも異なる関数へとルーティングできます。methods を指定しない場合、GETリクエストのみルーティングされます。

@app.route('/edit', methods=['GET'])
def get_edit():
    # GET時の処理

@app.route('/edit', methods=['POST'])
def post_edit():
    # POST時の処理

処理の内容にもよりますが、私は、HTTPメソッドに対応した関数を定義するほうが好きです。GETとPOSTで異なる処理をすることがほとんどであり、ifで分岐させるよりも、関数を呼び出す時点で分岐しておいた方がスッキリしていると感じます。HTTPメソッドごとに関数を定義する場合は、HTTPメソッドをプレフィックスとした関数名にしておくとわかりやすいです。

変数付きのルーティング

たとえば、User ID=1のデータをリクエストと一緒に送信したいという場合があります。その場合、値をURLの一部として渡す、もしくは、クエリストリングとして渡すのが一般的かと思います。前者のように http://xxx/edit/1 のようにデータを渡したいという場合は、変数付きのルーティングを用います。

@app.route('/edit/<user_id>')
def edit(user_id):
    # user_idを使って処理をする。user_idは文字列として渡される

@app.route('/edit/<int:user_id>')
def edit(user_id):
    # user_idは int として渡される

2つ目のようにconverter を指定することも可能です。converterが指定されていない場合、stringとして扱われます。

また、変数にはデフォルト値を設定できます。ひとつの関数に対して、複数のURLを紐づけることもできます。この2つを組み合わせると、以下のようなルーティングも可能です。

@app.route('/list')
@app.route('/list/<int:page>')
def get_list(page=1):
    # <int:page> が未指定の場合、page=1が設定される

上記の場合、http://xxx/list にアクセスすると、page = 1 になります。

扱う変数の型が明確な場合には、converterを指定しておくと良いです。converterに指定した型と異なる型のデータがURLとして渡された場合、 404 NotFound を返します。上記の例ですと、user_idに aaa などの文字列が渡された場合、 404 Not Found を返します。converterが利用できる場合は、積極的に利用していきましょう。

静的ファイルのルーティング

CSSJavaScript、画像といった静的ファイルのルーティングは自動でされます。

デフォルトでは、 http://xxx/static が static ディレクトリに対応しています。Flaskアプリケーションに static という名前のディレクトリを作成すると自動的にルーティングされます。前述したような route() による定義は必要ありません。

FlaskApp
└── static
    ├── css
    │   └── test.css
    ├── js
    │   └── test.js
    └── test.png

static以下のディレクトリは、ディレクトリ名がパスとしてルーティングされます。上記構成の場合、以下のURLが有効です。

  • http://xxx/static/test.png
  • http://xxx/static/css/test.css
  • http://xxx/static/js/test.js

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

はじめに

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

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

この記事は、Flaskのざっくりとした紹介のみです。

Flaskについて

Flaskは、Pythonのための microframework です。

micro の名前通り、Webアプリケーション開発に必要な最低限の機能のみを提供しています。具体的には以下のような機能です。

ちょっと込み入ったWebアプリケーションを作るといずれ必要になるであろう「入力フォームの検証」や「データベースの抽象レイヤー」などは用意されていません。代わりに、まるでFlask自身の機能であるかのように、それらの機能を追加・拡張できる仕組みが用意されています。既存のFlaskの拡張は以下にまとめられています。

Extensions Registry | Flask (A Python Microframework)

また、Pythonには既に多くのライブラリが存在しています。Flaskにない機能については、ライブラリを利用するのも手です。ありがたいことに、Pythonの多種多様なライブラリは下記リポジトリにリストアップされています。

github.com

「不足機能を別のライブラリで補う」と聞くと、「Pythonの各種ライブラリに精通していないような初学者には難しいのではないか」と感じる人もいるかもしれません。しかし、それは杞憂です。むしろ、micro であるがゆえに初期の学習コストは低く、初学者が学習目的で使うこともできます。手っ取り早く動くコードを書いて、振る舞いを確認したいというのであれば、Flaskのようなmicroframeworkが適していると思います。

さいごに

次回以降、気分で箇条書きした各機能について触れていこうと思います。

Layer Collision Matrixを使って雑に「緊急回避」を実現する

「緊急回避」とは、アクションゲームでよくある「前転時などに一定時間無敵になり、敵の攻撃回避を目的としたアクション」のことを指しています。 ほかにも良い方法があるかもしれませんが、お手軽さを優先して、対象のGameObjectのレイヤーを切り替える方法を取りました。「ダメージを受けてノックバックした時の無敵状態」の実現などにも使えます。

なお、私が作成しているゲームが2Dであるため、2Dを題材としています。

Layer Collision Matrixを設定する

Layer Collision Matrix とは

Layer Collision Matrix を使うことで、Colliderが衝突検知するか否かをLayerレベルで設定できます。Layer Collision Matrix の設定項目は、 Edit > Project Settings > Physics 2D で開かれる inspector に存在します。

f:id:subarunari:20170701003421p:plain:w300

Default, TransparentFX, ignore Raycast, Water, UI 5つは Built in なレイヤーです。レイヤー名の交点にあるチェックボックスは「衝突検知するか否か」を表しています。チェックが入っている場合に衝突を検知します。デフォルトではすべてチェックが入っているので、すべてのレイヤー間で衝突が検知されます。

雑な緊急回避を実装するにあたって、以下の2つのLayerを新たに作成しています。

  • Damage Layer : ダメージを与える物体が存在するレイヤー
  • Dodge Layer : 緊急回避時に一時的に切り替えるレイヤー

Layerの作成は、Game ObjectのInspector上にある Layerのドロップダウンリスト > Add Layer… からできます。

衝突回避の設定

衝突を検知したくないレイヤーの交点のチェックボックスを外します。今回は、緊急回避時のレイヤーとして「Dodge Layer」を使うので、「Dodge Layer」と「Damage Layer」の交点となるところのチェックを外します。これで下準備は終わりです。

f:id:subarunari:20170701011133p:plain:w300

コードからGameObjectのレイヤーを切り替える

以下のコードでレイヤーの切り替えは簡単にできます。レイヤーを切り替えたら元に戻すのを忘れずに。

// レイヤー番号を名前から取得し、GameObjectのlayerプロパティに指定する。
gameObject.layer = LayerMask.NameToLayer("DodgeLayer");

与えられた名前のレイヤーが見つからない場合、NameToLayerメソッドは -1 を返します。gameObject.layerに -1 が設定されると以下のエラーが吐かれます。

A game object can only be in one layer. The layer needs to be in the range [0...31]
UnityEngine.GameObject:set_layer(Int32)

NameToLayerメソッドを使えば、うっかり名前を変えてしまった場合にすぐ気付けますし、なんらかの事情でレイヤーの順番を変更しなければならない場合にコードレベルの対応も必要ありません。

雑なサンプル

はじめにこんな感じで、プレイヤーにダメージを与えることのできるGameObjectのLayerを 「DamageLayer」 にしておきます。プレイヤーのLayerはDefaultのままです。

f:id:subarunari:20170701015643p:plain

続いてコードです。単純にするために対応するキーを押した時にレイヤーが切り替わるようにしています。「Z」キーを押したらレイヤーが DodgeLayer に変わり、衝突が検知されなくなります。

public class Sample: MonoBehaviour
{
    void Update()
    {
        GetComponent<Rigidbody2D>().velocity = new Vector2(Input.GetAxis("Horizontal") * 5, 0);
        if(Input.GetKeyUp(KeyCode.Z))
        {
            // Zキーを押したら、DodgeLayerに切り替え
            gameObject.layer = LayerMask.NameToLayer("DodgeLayer");
            Debug.Log(gameObject.layer);
        } 
        else if (Input.GetKeyUp(KeyCode.X))
        {
            // Xキーを押したら、DefaultLayerに戻す
            gameObject.layer = LayerMask. NameToLayer("Default");
            Debug.Log(gameObject.layer);
        }
    }
}

今回は単純化のためにキーを押した時にレイヤーを切り替えていますが、実際は「緊急回避」のアクション開始時と終了時に、レイヤーの切り替えを行うことになるかと思います。

まとめ

以上、Layer Collision Matrixを用いた雑な緊急回避の実現でした。

今回は、DamageLayerを用意しましたが、ダメージを与えうる全てのGameObjectにLayerを設定するのは骨が折れます。DamageLayerの代わりに、床、壁、天井などを表すFloorLayerを用意し、DefaultとDodgeLayerのチェックを外すという手もあります。どちらもLayer設定の手間はかかってしまいますが・・・。設定量が少なく済むようにすれば幸せになれそうです。

補足

Collider 2Dの基礎についてはこちらの記事に記載しています。

subarunari.hatenablog.com