Jinja2 のテンプレートを書く際に、CoffeeScript の存在演算子のようなものが欲しくなったのでそれっぽいカスタムフィルタを実装してみたと言う小ネタ。
問題点
例えば「新規作成」「編集」ページで同じテンプレートを使いまわしたい場合に、「新規作成」のページではフォームの中身は空でいい (もしくはデフォルト値を入力する) が、「編集」ページでは既存データをフォームに入力した状態で表示したい。
data = {
'foo': {
'bar': 123
}
}
ただしこのとき、既存データは data
という変数に複雑な構造で格納されており、その存在確認をした上でフォームの入力値を出し分けようとすると以下のような条件式を書かなければならない。
{% if 'foo' in data and 'bar' in data['foo'] %}
<input type="text" name="foo.bar" value="{{ data['foo']['bar'] }}" />
{% else %}
<input type="text" name="foo.bar" value="0" />
{% endif %}
これはとても手間がかかるし、可読性が低いし、記述が重複しているし、コードの行数もかなり長くなってしまう。良いことがない。
これをもう少し綺麗にまとめることができないかと考えた。
CoffeeScript の存在演算子
CoffeeScript には存在演算子 (Existential Operator) と言う演算子があり1、識別子に対して ?
を末尾につけると undefined
かどうかをチェックしてくれて、さらにその後でプロパティアクセスされていてもすべて undefined
を返してくれる。
obj = foo: bar: 123 # { foo: { bar: 123 } }
obj.foo.bar # 123
obj.foo.foo # undefined
obj.foo.foo.foo # TypeError: Cannot read property 'foo' of undefined
obj.foo.bar? # true
obj.foo.foo? # false
obj.foo.foo?.foo # undefined
obj.foo.foo?.foo.foo # undefined
これは「データがない場合はとにかく undefined
が欲しい」と言う場合にとても便利で、特にアプリケーションのビューを実装する場合にこのような機能が欲しくなる。
このような機能を Jinja2 でも実装できたら上述の問題点を解決できそうな気がする。
実装
Jinja2 で ?
のような演算子を実装するわけにはいかないので、今回はカスタムフィルタとして実装する。
from jinja2.runtime import Undefined
def dig_nested_dict(d: dict, key: str):
""" ネストされた dict オブジェクトから指定されたキーに対応する要素を返す
例:
key = 'foo.bar' を指定したとき、d['foo']['bar'] が存在すればその値を返す
存在しなければ jinja2.runtime.Undefined オブジェクトを返す
"""
if type(d) == Undefined:
return Undefined()
v = d
for k in key.split('.'):
if k in v:
v = v[k]
else:
return Undefined()
return v
値が存在しないときに None
や ''
ではなく jinja2.runtime.Undefined
オブジェクトを返すのは重要で、こうすることで Jinja2 の組み込みフィルタ default と組み合わせて使うことができるようになる。(使用例は後述)
次に、この関数を Jinja2 のカスタムフィルターとして登録する。
例えば aiohttp と組み合わせる場合は aiohttp_jinja2 を使って以下のように書ける。
aiohttp_jinja2.setup(app, filters={'dig': dig_nested_dict})
ここではテンプレート内で dig
と言う名前のフィルターが使えるように登録した。
使用例
登録した dig
と Jinja2 組み込みの default
フィルタを組み合わせると、冒頭のテンプレートは以下のように書き直せる。
<input type="text" name="foo.bar" value="{{ data | dig('foo.bar') | default('0') }}" />
5行だったコードが1行になったし、可読性も高くなったと思う。
-
Optional Chaning という名前で JavaScript にも採用されそう らしい ↩