Flaskでデコレーターを使うとうまく動かなかった件とその対策
Flaskでデコレーターを使う方法を調べてみた。
発端
- Flaskでデコレーターを使ってみた
- うまく動いてる
- パスを増やした
- あれ?全部のパスで同じ関数呼ばれてない!?
- もしかしてデコレーター使えないの!?
ちょっと検索するとFlaskでデコレーターを使う方法は見つかったんだけど、どういう理屈なのかの説明は見つけられなかったので調べてみた。
こういう物事を説明するのは絶望的に下手なんだけど調べた以上は書き留めたくて。。
でもって間違っているかもしれなくて。
問題と対策
ハマったときのコード
from flask import Flask app = Flask(__name__) app.debug = True def sample_decolator(fnc): def decorate(*args, **kwargs): print 'sample_decolator' return fnc(*args, **kwargs) return decorate @app.route('/page1') @sample_decolator def page1(): print 'page1' return 'page1' @app.route('/page2') @sample_decolator def page2(): print 'page2' return 'page2' if __name__ == '__main__': app.run()
/page1、/page2のどちらにアクセスしてもpage2()が実行される。
困った。
「Flask decolator」で検索してみるとfunctoolsのwrapsというデコレーターを使っているコードを見つけたので試しに使ってみると問題は解決。
from flask import Flask app = Flask(__name__) app.debug = True def sample_decolator_w_wraps(fnc): @wraps(fnc) def decorate(*args, **kwargs): print 'sample_decolator_w_wraps' return fnc(*args, **kwargs) return decorate @app.route('/page1') @sample_decolator_w_wraps def page1(): print 'page1' return 'page1' @app.route('/page2') @sample_decolator_w_wraps def page2(): print 'page2' return 'page2' if __name__ == '__main__': app.run()
正しくpage1()とpage2()が実行される。
なぜ?
日を改めて、なぜfunctools.wraps()を使えば解決するのか、なぜ今までデコレーターを使ってきたときに同じ問題が起きなかったのかを調べてみた。
- 1:デコレーターとfunctools.wraps()
まずfunctools.wraps()がなにをしているかを調べてみる。
元々のコード
def decoratetest(fnc): def decorate(*args, **kwargs): print 'decorate' return fnc(*args, **kwargs) return decorate @decoratetest def func1(): print '-- func1' @decoratetest def func2(): print '-- func2' func1() print func1.__name__ print func1 func2() print func2.__name__ print func2 # decorate # -- func1 # decorate # <function decorate at 0x4034c844> # decorate # -- func2 # decorate # <function decorate at 0x4034c4fc>
デコレーターdecolate()を付けたfunc1()、func2()はそれぞれdecolate()として見えている。
これはfunc1()として呼ばれる関数はdecolate()であり、decolate()の戻り値がfunc1()のため、__name__はfunc1ではなくdecolorateになっている。(返ってきた関数を実行するためdecolate()を実行したのではなくfunc1()を実行したように見える)
これを避けるためにfunctools.partial()が用意されており、簡単に適用できるようデコレーターの形でfunctools.wraps()が提供されている。
参考:Pythonのドキュメントfunctools内のpartial() http://docs.python.jp/2.5/lib/module-functools.html
wrapsを使ってみる
from functools import wraps def decoratetest(fnc): def decorate(*args, **kwargs): print 'decorate' return fnc(*args, **kwargs) return decorate def decoratetest_wraps(fnc): @wraps(fnc) def decorate(*args, **kwargs): print 'decorate' return fnc(*args, **kwargs) return decorate @decoratetest def func1(): print '-- func1' print func1.__name__ @decoratetest def func2(): print '-- func2' print func2.__name__ @decoratetest_wraps def func3(): print '-- func3' print func3.__name__ func1() print func1.__name__ print func1 func2() print func2.__name__ print func2 func3() print func3.__name__ print func3 # decorate # -- func1 # decorate # decorate # <function decorate at 0x40354224> # decorate # -- func2 # decorate # decorate # <function decorate at 0x40354294> # decorate # -- func3 # func3 # func3 # <function func3 at 0x40354304>
wrapsを使うとラップされた内側の関数名「func3」として見える。
functools.wraps()はラップされた関数の名前をラップしている外側の関数に引き継いでくれる。(「decolator()がfunc3()を返す」構造は変わっていないがdecolator().__name__がfunc3になる。)
デコレーターは関数を返す関数なので、高階関数のために用意されたfunctoolsモジュールが助けになる。
- 2:FlaskのURLマッピングの仕組み
@app.route('/page') def page(): print 'page' return 'page'
/pageをpage()でハンドルする場合こんな風になる。
Flask内部でのデータの持ち方
-
- Flaskはパスとハンドルする関数のマッピングにwerzkeugのrouting.pyで定義されているMapとRuleクラスを使っている。
- このRuleクラスは処理をおこなう関数をendpointというプロパティとして保持しているが、このプロパティはどのような型でも良い(関数への参照でも、関数の名前でも)
Flask内での処理
-
- Flaskでは1行目の様にapp.route()デコレーターを使ってパスを指定する(app.route()はルーティングの登録のみをおこなうため、Flask.add_url_rule()にパスとエンドポイント、その他のパラメーターを渡すだけの処理をしている)
- add_url_rule()は、エンドポイントを正規化するため、受け取ったエンドポイントをhelpers.pyで定義された_endpoint_from_view_func()に渡す。
- _endpoint_from_view_func()は渡された関数の名前を文字列として返す仕様になっているため、add_url_rule()内でRuleインスタンスの作成する際にはにエンドポイントとして指定された関数の名前が使われる。(Ruleのendpointプロパティに関数の名前が文字列で格納される)
というわけで、@app.route()デコレーターを使った場合、URLと関数のマッピングに関数の「名前」が使用されている。
- 3:デコレーターの動きをFlaskで見てみる
Flaskでデコレーターを使うとどのようにURLにマップされているかを確認してみる。
# -*- coding: utf-8 -*- from flask import Flask from functools import wraps from pprint import pformat app = Flask(__name__) app.debug = True def sample_decolator(fnc): def decorate(*args, **kwargs): print 'sample_decolator' return fnc(*args, **kwargs) return decorate def sample_decolator_w_wraps(fnc): @wraps(fnc) def decorate(*args, **kwargs): print 'sample_decolator_w_wraps' return fnc(*args, **kwargs) return decorate @app.route('/page') def page(): print 'page' return 'page' @app.route('/page1') @sample_decolator def page1(): print 'page1' return 'page1' @app.route('/page2') @sample_decolator def page2(): print 'page2' return 'page2' @app.route('/page3') @sample_decolator_w_wraps def page3(): print 'page3' return 'page3' @app.route('/page4') @sample_decolator_w_wraps def page4(): print 'page4' return 'page4' @app.route('/dump') def dump(): _dump = pformat(app.url_map) print _dump return '<textarea>' + _dump + '</textarea>' if __name__ == '__main__': app.run()
/dumpにアクセスすると以下のURLマップの情報が取得できる
Map([<Rule '/page1' (HEAD, OPTIONS, GET) -> decorate>, <Rule '/page2' (HEAD, OPTIONS, GET) -> decorate>, <Rule '/page3' (HEAD, OPTIONS, GET) -> page3>, <Rule '/page4' (HEAD, OPTIONS, GET) -> page4>, <Rule '/page' (HEAD, OPTIONS, GET) -> page>, <Rule '/dump' (HEAD, OPTIONS, GET) -> dump>, <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>])
wrapsしていないデコレーターを使ったpage1、page2にはデコレーター関数の名前が渡っている。
page3、page4で使ったデコレーターはwrapsを使ったため、本来の関数の名前が正しく渡っている。
- 全部あわせると
・FlaskのURLマッピングはデコレーター経由で渡された関数の「名前」を保持する
・デコレーターを使うとき、デコレーター関数の名前でデコレート対象の関数の名前が上書きされるように見える(例ではsample_decolator()がreturnするのはデコレートされた関数を返すdecolate()関数で、そのdecolate()関数の__name__プロパティはdecolateになる)
の2点が組み合わさって今回の問題が発生していたことが分かった。
Flaskをちゃんと使ったのはこれが始めてで、最初はらくちんさに感激していたものの、落とし穴に途中でハマった時はさすがに焦った。(Authorizationヘッダーを常にチェックするためにデコレーターを使って共通化していた)
デコレーターはそれなりに作ってはいたけれど、呼ばれるときの関数の名前を意識する必要は今まで無く、functoolsのお世話になったこともなかった。まだまだ知らない事は多い。