BeProud Advent Calender 2018 9日目の記事です。
案件でDjangoを使っていたときにdefaultdictがtemplateにレンダリングできないことに気づいたので検証してみた。
案件で使っているバージョンは1.9.3。
おしながき
ディレクトリ構成
Django 1.9.3, Django 2.1.4, jinja2で確かめるということで変則的なディレクトリ構成になっている。 mysite以下はDjangoプロジェクトの通常の構成。
advent_cal_2018 ├ django_1.9.3 │ ├ mysite │ └ env ├ django_2.1.4 │ ├ mysite │ └ env └ jinja2 ├ env └ defaultdict_survey.py
検証用のWebアプリ
localhost:8000
に繋ぐと辞書の中身を表示するだけのWebアプリをでっち上げた。
# mysite/myapp/views.py from collections import defaultdict, OrderedDict from django.shortcuts import render def index(request): # dictionary normal_d = { 'foo': 'foo', 'bar': 'bar', 'baz': 'baz', } # defaultdict(list) default_d_1 = defaultdict(list) s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)] for k, v in s: default_d_1[k].append(v) # defaultdict(str) default_d_2 = defaultdict(int) keys = ['ham', 'spam', 'eggs', 'ham', 'spam', 'spam', 'eggs'] for k in keys: default_d_2[k] += 1 # OrderedDict ordered_d = OrderedDict() s = [(0, 'zero'), (1, 'one'), (2, 'two')] for k, v in s: ordered_d[k] = v context = { 'normal': normal_d, 'default_d_list': default_d_1, 'defaukt_d_int': default_d_2, 'ordered_d': ordered_d, } return render(request, 'myapp/index.html', context)
<!--mysite/myapp/temaplates/myapp/index.html--> <html> <head> <title>BP Advent Calendar 2018</title> </head> <body> <div class="normal"> <h1>dictionary</h1> <table> <tr><th>key</th><th>value</th></tr> {% for k, v in normal.items %} <tr><td>{{ k }}</td><td>{{ v }}</td></tr> {% endfor %} </table> </div> <div class="defaultdict-list"> <h1>defaultdict(list)</h1> <table> <tr><th>key</th><th>value</th></tr> {% for k, v in default_d_list.items %} <tr><td>{{ k }}</td><td>{{ v }}</td></tr> {% endfor %} </table> </div> <div class="defaultdict-int"> <h1>defaultdict(int)</h1> <table> <tr><th>key</th><th>value</th></tr> {% for k, v in default_d_int.items %} <tr><td>{{ k }}</td><td>{{ v }}</td></tr> {% endfor %} </table> </div> <div class="OrderedDict"> <h1>OrderedDict</h1> <table> <tr><th>key</th><th>value</th></tr> {% for k, v in ordered_d.items %} <tr><td>{{ k }}</td><td>{{ v }}</td></tr> {% endfor %} </table> </div> </body> </html>
Django 1.9.3で確認する
$ pwd /Users/user/Python/works/advent_cal_2018/django_1.9.3 $ ls env mysite requirements.txt $ . env/bin/activate (env) $ cd mysite/ (env) $ pip list Package Version ---------- ------- Django 1.9.3 pip 10.0.1 setuptools 39.0.1 You are using pip version 10.0.1, however version 18.1 is available. You should consider upgrading via the 'pip install --upgrade pip' command. (env) $ python manage.py runserver Performing system checks... System check identified no issues (0 silenced). December 09, 2018 - 05:46:05 Django version 1.9.3, using settings 'mysite.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.
表示された画面
確かにdefaultdictはレンダリングされていない。
Django 2.1.4で確認する
$ pwd /Users/user/Python/works/advent_cal_2018/django_2.1.4 $ ls env mysite requirements.txt $ . env/bin/activate (env) $ cd mysite/ (env) $ pip list Package Version ---------- ------- Django 2.1.4 pip 10.0.1 pytz 2018.7 setuptools 39.0.1 You are using pip version 10.0.1, however version 18.1 is available. You should consider upgrading via the 'pip install --upgrade pip' command. (env) nao-Mac:mysite nao$ python manage.py runserver Performing system checks... System check identified no issues (0 silenced). December 09, 2018 - 05:52:18 Django version 2.1.4, using settings 'mysite.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.
表示された画面
2.1.4でもdefaultdictはレンダリングできないっぽい。
Jinja2で試してみる
Jinja2ではテンプレートにdefaultdictを埋め込んで、HTMLを出力することで確認する。
# jinja2/defaultdict_survey.py from collections import defaultdict, OrderedDict from jinja2 import Template template = """ <html> <head> <title>BP Advent Calendar 2018</title> </head> <body> <div class="normal"> <h1>dictionary</h1> <table> <tr><th>key</th><th>value</th></tr> {% for k, v in normal.items() %} <tr><td>{{ k }}</td><td>{{ v }}</td></tr> {% endfor %} </table> </div> <div class="defaultdict-list"> <h1>defaultdict(list)</h1> <table> <tr><th>key</th><th>value</th></tr> {% for k, v in default_d_list.items() %} <tr><td>{{ k }}</td><td>{{ v }}</td></tr> {% endfor %} </table> </div> <div class="defaultdict-int"> <h1>defaultdict(int)</h1> <table> <tr><th>key</th><th>value</th></tr> {% for k, v in default_d_int.items() %} <tr><td>{{ k }}</td><td>{{ v }}</td></tr> {% endfor %} </table> </div> <div class="OrderedDict"> <h1>OrderedDict</h1> <table> <tr><th>key</th><th>value</th></tr> {% for k, v in ordered_d.items() %} <tr><td>{{ k }}</td><td>{{ v }}</td></tr> {% endfor %} </table> </div> </body> </html> """ template = Template(template) # dictionary normal_d = { 'foo': 'foo', 'bar': 'bar', 'baz': 'baz', } # defaultdict default_d = defaultdict(list) s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)] for k, v in s: default_d[k].append(v) # defaultdict(list) default_d_1 = defaultdict(list) s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)] for k, v in s: default_d_1[k].append(v) # defaultdict(str) default_d_2 = defaultdict(int) keys = ['ham', 'spam', 'eggs', 'ham', 'spam', 'spam', 'eggs'] for k in keys: default_d_2[k] += 1 # OrderedDict ordered_d = OrderedDict() s = [(0, 'zero'), (1, 'one'), (2, 'two')] for k, v in s: ordered_d[k] = v context = { 'normal': normal_d, 'default_d': default_d, 'ordered_d': ordered_d, } rendered = template.render( normal=normal_d, default_d_list=default_d_1, default_d_int=default_d_2, ordered_d=ordered_d) print(rendered)
$ cd $ pwd /Users/user/Python/works/advent_cal_2018/jinja2 $ ls defaultdict_survey.py env requirements.txt $ python defaultdict_survey.py > rendered.html
<!--jinja2/rendered.html--> <html> <head> <title>BP Advent Calendar 2018</title> </head> <body> <div class="normal"> <h1>dictionary</h1> <table> <tr><th>key</th><th>value</th></tr> <tr><td>foo</td><td>foo</td></tr> <tr><td>bar</td><td>bar</td></tr> <tr><td>baz</td><td>baz</td></tr> </table> </div> <div class="defaultdict-list"> <h1>defaultdict(list)</h1> <table> <tr><th>key</th><th>value</th></tr> <tr><td>yellow</td><td>[1, 3]</td></tr> <tr><td>blue</td><td>[2, 4]</td></tr> <tr><td>red</td><td>[1]</td></tr> </table> </div> <div class="defaultdict-int"> <h1>defaultdict(int)</h1> <table> <tr><th>key</th><th>value</th></tr> <tr><td>ham</td><td>2</td></tr> <tr><td>spam</td><td>3</td></tr> <tr><td>eggs</td><td>2</td></tr> </table> </div> <div class="OrderedDict"> <h1>OrderedDict</h1> <table> <tr><th>key</th><th>value</th></tr> <tr><td>0</td><td>zero</td></tr> <tr><td>1</td><td>one</td></tr> <tr><td>2</td><td>two</td></tr> </table> </div> </body> </html>
Jinja2では問題なくdefaultdictを使える。
Djangoでdefaultdictを使いたいとき
単純にdefaultdictをdictionaryに変換してあげるのが一つ。
テンプレートエンジンにJinja2を設定できるのでそれを利用する手もある。
https://docs.djangoproject.com/en/2.1/topics/templates/#django.template.backends.jinja2.Jinja2
ただ、すべてのテンプレートをJinja2に合わせる必要があるので、基本的にはdictionaryに変換することになる、のかなぁ。
なぜdefaultdictとOrderedDictで扱いが違うのか。
OrderedDict
通常の dict メソッドをサポートする、辞書のサブクラスのインスタンスを返します。
defaultdict
新しいディクショナリ様のオブジェクトを返します
collections --- コンテナデータ型 — Python 3.7.1 ドキュメント
このあたりに理由がありそう。
OrderedDictが 辞書のサブクラスであるのに対してdefaultdictは 新しいディクショナリ様のオブジェクト。
字面だけ見るとdefaultdictは辞書のようで実際は辞書でない、と読める。Djangoのテンプレートエンジンは辞書とそのサブクラスには対応できるが、「ディクショナリ様のオブジェクト」には対応していない、のかな。
原因を把握するためにはdefaultdictとOrderedDictの実装、Djangoのテンプレートエンジンでの辞書の扱いを理解する必要がありそう。