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

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

いまさらながら 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>}
    }
}