蛇ノ目の記

技術のことも。そうでないことも。

Djangoのtemplateでdefaultdictを使えない件について

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アプリをでっち上げた。

# 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.

表示された画面 f:id:Nao_Y:20181209144824p:plain

確かに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.

表示された画面 f:id:Nao_Y:20181209145345p:plain

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のテンプレートエンジンでの辞書の扱いを理解する必要がありそう。