Edited at

Jinja2 で dict の要素にアクセスしやすくなるカスタムフィルタ

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 を返してくれる。


CoffeeScript

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行になったし、可読性も高くなったと思う。





  1. Optional Chaning という名前で JavaScript にも採用されそう らしい