BeProud Advent Calender 2018 9日目の記事です。
adventar.org
案件で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アプリをでっち上げた。
from collections import defaultdict, OrderedDict
from django.shortcuts import render
def index(request):
normal_d = {
'foo': 'foo',
'bar': 'bar',
'baz': 'baz',
}
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)
default_d_2 = defaultdict(int)
keys = ['ham', 'spam', 'eggs', 'ham', 'spam', 'spam', 'eggs']
for k in keys:
default_d_2[k] += 1
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)
<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>
$ 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はレンダリングされていない。
$ 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を出力することで確認する。
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)
normal_d = {
'foo': 'foo',
'bar': 'bar',
'baz': 'baz',
}
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)
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)
default_d_2 = defaultdict(int)
keys = ['ham', 'spam', 'eggs', 'ham', 'spam', 'spam', 'eggs']
for k in keys:
default_d_2[k] += 1
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
<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のテンプレートエンジンでの辞書の扱いを理解する必要がありそう。