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モジュールが助けになる。


@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のお世話になったこともなかった。まだまだ知らない事は多い。