読者です 読者をやめる 読者になる 読者になる

デコレータを作ってみる

前回の記事(Pythonのデコレータを理解するときに残したメモ - kk6のメモ帳*)でデコレータについて理解したので、じゃあ今度はデコレータを作ってみようと思う。あんまり詳しくないけどflaskを題材としてみる。

# hello.py
from flask import Flask
from flask import render_template

app = Flask(__name__)

@app.route('/<name>')
def hello(name):
    return render_template('hello.html', name=name)

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

htmlはなんか適当に下みたいな感じに用意してください。

<!DOCTYPE HTML>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Hello Flask</title>
</head>
<body>
  {% if name %}
    <h1>Hello {{ name }}!</h1>
  {% else %}
    <h1>Hello World!</h1>
  {% endif %}
</body>
</html>

構成はこんな感じ

├── hello.py
└── templates
     └── hello.html

flaskのview関数って基本的に return render_template('hoge.html', hoge=hoge)とかすると思うんだけど、それは面倒なのでview関数はシンプルにテンプレートに渡すコンテキストの辞書を返すようにして、その辞書とテンプレートを受け取ってrender_templateを実行するデコレータを作ってみる。

具体的にはこんな感じになるように実装する。

@app.route('/<name>')
@render('hello.html')
def hello(name):
    return {
        'name': name,
    }

考え方というか説明の仕方として二通り思いついたんだけど、どっちがわかりやすいか判断つかなかったので一応両方掲載。個人的には後者のほうがわかりやすいような気もする。

考え方その1:内側から組み立てる

まず、最終的に実行したいものを明確にする。view関数が返す辞書をrender_templateに渡して実行したい。

ctx = view_func(*args, **kw)
render_template(template, **ctx)

まずはこれを関数にする

def wrapped(*args, **kw)
    ctx = view_func(*args, **kw)
    return render_template(template, **ctx)

この時点で足りないのはview_funcとtemplate。view_funcはデコレート対象の関数。templateはデコレータ自身に渡す引数。なのでまずはview関数を引数に受取る関数で包む。(@render('hello.html')みたいな使い方のデコレータを作りたいわけなので、当然最も外側の関数名はrenderになるし、その引数はテンプレートのパスになる。必然的にview関数を受け取る関数はその内側にネストしてなくてはならなくなる)

def wrapper(view_func):
    @wraps(view_func)
    def wrapped(*args, **kw)
        ctx = view_func(*args, **kw)
        return render_template(template, **ctx)
    return wrapped

さらにデコレータ自身に渡す引数を受け取る関数で包む

def render(template)
    def wrapper(view_func):
        @wraps(view_func)
        def wrapped(*args, **kw)
            ctx = view_func(*args, **kw)
            return render_template(template, **ctx)
        return wrapped
    return wrapper

考え方その2:外側から組み立てる

まず、@render('hello.html')という使い方をしたいので、最も外側の関数は

def render(template):
    ....
    return ...

のようになる。続いて、その内部にview関数を受け取る関数wrapperを用意し、renderがwrapperを返すようにする。

def render(template):
    def wrapper(view_func):
        ...
        return ...
    return wrapper

さらに、元の関数の引数を受け取る関数wrappedを用意し、wrapperがwrappedを返すようにする。

def render(template):
    def wrapper(view_func):
        @wraps(view_func)
        def wrapped(*args, **kw):
            ...
            return ...
        return wrapped
    return wrapper

これでテンプレート(template)、view関数(view_func)、view関数の引数(*args, **kw)が揃ったので、あとはやりたい処理(render_templateの実行)を記述するだけ。

def render(template)
    def wrapper(view_func):
        @wraps(view_func)
        def wrapped(*args, **kw)
            ctx = view_func(*args, **kw)
            return render_template(template, **ctx)
        return wrapped
    return wrapper

ここからは余談

デコレータの作り方としては以上なんだけど、これだとview関数の戻り値が辞書じゃない場合(return redirect(url_for('hogehoge'))とかしてBase Responseとかが返ってくる場合)に、それをrende_templateに渡そうとしてしまってエラーになったりするので、辞書以外が来た場合はそのまま返すように手を加える。

def render(template)
    def wrapper(view_func):
        @wraps(view_func)
        def wrapped(*args, **kw)
            ctx = view_func(*args, **kw)
            if isinstance(ctx, dict):
                return render_template(template, **ctx)
            else:
                return ctx
        return wrapped
    return wrapper

これでひとまず使えるものができたと思います。flask詳しくないので何か足りないかもしれないけど、それはその都度拡張・修正ということで。