蛇ノ目の記

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

SpotifyのAPIを使ってみた

最近Spotifyを契約し始めたのだけど、Spotifyには曲やアルバム、プレイリスト、アーティストの情報を取得したりできるAPIがあるとのことなので試してみた。

Spotipyというラッパーライブラリがあるようだけど、今回はそれを使わずにrequestsを使ってAPIを使ってみる。

アプリの登録

APIを使うにはFreeかPremiumのユーザーである必要がある。登録はhttps://www.spotify.com/jp/から。

ユーザー登録したらダッシュボードページにログインして、アプリ登録をする。

f:id:Nao_Y:20190117234242p:plain

作成したアプリをクリックする。

f:id:Nao_Y:20190117234313p:plain

Client IDSHOW CLIENT SECRETを押して表示されるClient Secretを控えておく。

認証フロー

Spotify APIには3つの認証フローがある。

  • Refreshable user authorization: Authorization Code Flow

長期間稼働するアプリ向けの認証。エンドユーザーは一度だけアプリがリソースにアクセスすることを許可する。アプリにはリフレッシュ可能なアクセストークンが提供される。バックエンドなど安全な場所から実行する。

  • Temporary user authorization: Implicit Grant

JavaScriptを使用するため、リソース所有者のブラウザ上で動作するアプリ向けの認証。

  • Refreshable app authorization: Client Credentials Flow

サーバー間認証で使用される。エンドユーザーのリソースにアクセスしないエンドポイントのみ使用可能。

参考:

アクセストークンの取得

今回はClient Credentials Flowを使う。この認証方法ではClient IDとSecret Keyを使ってAccess Tokenを取得する。

  • Endpoints: POST https://accounts.spotify.com/api/token

  • Request Body Parameter: grant_type(client_credentialsを設定する)

  • Header Parameter: Basic <base64 encoded client_id:client_secret>

import requests

CLIENT_ID = 'YOUR CLIENT ID'

CLIENT_SECRET = 'YOUR CLIENT SECRET'

GRANT_TYPE = 'client_credentials'

TOKEN_URL = 'https://accounts.spotify.com/api/token'

body_params = {'grant_type': GRANT_TYPE}

auth = requests.post(TOKEN_URL, data=body_params, auth=(CLIENT_ID, CLIENT_SECRET)).json()

requestsでHTTP Basic auth(Basic認証)のあるURLにアクセスするときはキーワード引数auth(User, Password)とする。

この後、他のAPIで認証をするときにアクセストークンを使うので辞書形式でレスポンスを取得している。

参考:

アルバムを検索する

  • Endpoints: GET https://api.spotify.com/v1/search

  • Header Fields: Authorization

    • Value: 有効なアクセストークン。Bearer {{access token}}の形式。
  • Query Parameter(必須のものだけ抜粋)

    • q : 検索クエリ

    • type: 検索タイプ。有効な値はalbum, artist, playlist, trackでリストで指定可能

参考:

sora tob sakanaのアルバム・シングルを検索してみる。

header_params = {'Authorization': 'Bearer {}'.format(auth['access_token'])}

query_params = {'q': 'sora tob sakana', 'type': ['album']}

ENDPOINT = 'https://api.spotify.com/v1/search'

res = requests.get(ENDPOINT, headers=header_params, params=query_params)

for each in res.json()['albums']['items']:
    artist = each['artists'][0]
    url = each['external_urls']['spotify']
    print('{}: {} {} ({})'.format(artist['name'], each['name'], each['release_date'], url))
sora tob sakana: sora tob sakana 2016-07-26 (https://open.spotify.com/album/3cJxQS1oi8u2oBu9uhVyEk)
sora tob sakana: cocoon ep 2017-04-11 (https://open.spotify.com/album/467R6azC01pl67nSrLS55x)
sora tob sakana: mahou no kotoba 2016-02-16 (https://open.spotify.com/album/6x0xVz6hdHerGMf2yaMqLj)
sora tob sakana: yozora wo zenbu 2015-10-27 (https://open.spotify.com/album/2FezsFEuQFferFLy4syK4T)

どうしたことか。2017-04-11以降の作品が取得されない。よく見るとタイトルがアルファベット表記になっている。日本国内の設定になっていないっぽい。marketパラメータを指定してみる。このパラメータはISO 3166-1 alpha-2 country codeで表記する。日本はJP

header_params = {'Authorization': 'Bearer {}'.format(auth['access_token'])}

query_params = {'q': 'sora tob sakana', 'type': ['album']}

ENDPOINT = 'https://api.spotify.com/v1/search'

res = requests.get(ENDPOINT, headers=header_params, params=query_params)

for each in res.json()['albums']['items']:
    artist = each['artists'][0]
    url = each['external_urls']['spotify']
    print('{}: {} {} ({})'.format(artist['name'], each['name'], each['release_date'], url))

2017-04-11以降の作品が表示されて、タイトルも日本語表記になった。日本のアーティストを検索するならmarketJPを指定するのがよさそう。海外勢はalight epとかNew Stranger聴けないのかー。

sora tob sakana: sora tob sakana 2016-07-26 (https://open.spotify.com/album/6wy5MYR1ThaC7IuQu3VHHG)
sora tob sakana: New Stranger 2018-07-25 (https://open.spotify.com/album/1ATJgjPZ7Qz0LAUZtMW2Sp)
sora tob sakana: alight ep 2018-05-16 (https://open.spotify.com/album/36w9htd6tgXmNs0iPWte2P)
sora tob sakana: cocoon ep 2017-04-11 (https://open.spotify.com/album/3nN8No2a5ZhzK2xw9RIYQp)
sora tob sakana: アルファルド 2018-11-23 (https://open.spotify.com/album/3nA6Sd5gDIY8xHiVQQ3kvE)
sora tob sakana: 魔法の言葉 2016-02-16 (https://open.spotify.com/album/0UejaOY3nZq4h3fZ2be8lA)
sora tob sakana: 夜空を全部 2015-10-27 (https://open.spotify.com/album/03JuPAwApUfvx61cHvUmOJ)

APIを使ってアルバムを検索することができたので、この辺りで一旦区切りをつけておく。

今後はGET https://api.spotify.com/v1/searchにおけるワイルドカードの使い方(例えば、SawanoHiroyuki[nZk]:~をワイルドカードで検索)やプレイリストの検索を調べてみたい。

2019年ブログ初め

遅ればせながら、あけましておめでとうございます。

早々に「やってみた系」でアイデアが浮かんだんだけど、それより前にブログ初めをしておかねばという謎の義務感に駆られたので簡単に2019年にやりたいことを書いて2019年のブログ初めとする。

以下、目標と願望めいたやつを一緒くたに書きなぐる。

技術

  • 海外ブログの翻訳をやってみる

    • 去年末から着手してる。来月には公開できる、はず。
  • 積読の技術書を読む

    • Web, 機械学習いろいろ積んでる

    • 新しく買う前に読め

  • herokuを使わないWebサービスを作る

    • AWSのサービスをいろいろ使ってやってみたい

    • 2018年は仕事を通してEC2, route53, RDS, S3に触れたので経験を活かしたい

    • nginxもほんの少しわかるようになったのでそれも活かしたい

  • JavaScript覚える

    • そもそもJavaScript全然わからない

    • Vue.jsとか名前聞いたことあるレベルでしかないのでPythonのWebフレームワークと組み合わせる方法を身に付けたい

  • 英語を話す

    • オンライン英会話が気になってるけど、実際どういう感じかわからなくて手が出せてない

    • あと高そう

コミュニティ

  • stapyでイベント企画する

  • PyCon JPスタッフ業(3年目)がんばるぞい

    • コンテンツ盛り上げたい

    • 領収書発行サイトのカイゼン

  • もくもく会の参加率上げる

趣味

  • デジタルに手を出す

    • サブ機としてミラーレスを導入したい
  • 写真で同人誌出してみたい

    • C95で友人が出してたので続きがあるならいっちょ噛みする
  • 百合ポートレートを撮りたい

    • モデルのあてがビタイチない
  • 放置気味のギターを弾く


2019年もよろしくお願いします。

2018年を振り返る

年の瀬なので今年を振り返ってみる。という去年とまったく同じ書き出し。

nao-y.hatenablog.com

仕事

2018年はPythonエンジニアとして働いた初めての一年だった。

技術

Pythonだけでなく、AWS(IAM, S3, RDS, Route53)やnginxを触ることができた。

AWSではIAMユーザーを作ることを覚え、個人で作っているDiscord botでDynamoDBを使うときに応用できた。nginxではバーチャルサーバの基本的な設定に加えて、Let's Encryptによるサーバ証明書、OpenSSLによるクライアント証明書の有効化を覚えた。

執筆

今年の初めに書いたブログで「書く実績を解除する」ことを目標の一つにしていた。

ブログ初めには遅すぎた - 蛇ノ目の記

Software Design9月号の記事を書くという形で達成できた。編集者の方やレビュアーのBPメンバーには本当に感謝。

nao-y.hatenablog.com

本についてはもう一点。「スラスラ読める Pythonふりがなプログラミング」の監修をやらせていただいた。世の中に数多くあるPython入門本の入り口となる一冊で、超入門書といえる。

book.impress.co.jp

親にこの本の監修をしたことを話したところ、これまでプログラミング未経験だった父親が1冊通して読んでプログラミングの便利さを理解できたと言ってくれた。

Pythonコミュニティ

みんなのPython勉強会

1月のイベントで登壇した。

startpython.connpass.com

「Pythonでスタートして、Pythonを仕事にするまでの話」
横山 直敬(ビープラウド)
みんなのPython勉強会がきっかけで勉強を再開し、がっつりPythonをやる会社に入社しました。
Pythonによるキャリア形成の話やビープラウドに入ってからの話をします。

Pythonを独学して自分が面白いと思うことをやっていくうちにPythonが本業になった話をした。LTを目標に勉強する「LT駆動開発」なんて言葉を使っていたら、思いもよらないところで他の人に影響を与えていて本当に驚いた。

GitPitch Presents: github/NaoY-2501/GitPitch-Slides

The Fastest Way from Idea to Presentation for everyone on GitHub, GitLab, and Bitbucket.

PyCon APAC 2018

初の海外カンファレンス参加。そしてシンガポールに初上陸。アレな英語力を総動員して必死にトークを聴き取ろうとがんばっていた。

PyCon APAC 2018 カテゴリーの記事一覧 - 蛇ノ目の記

ここで聴いたFlaskによる機械学習WebアプリのトークはPyQの問題作成に活かすことができた。

blog.pyq.jp

帰国から即みんなのPython勉強会に参加して最速でPyCon APAC 報告LTをやった。

docs.google.com

PyCon JP 2018

2017年に引き続きスタッフとして参加。今年はコンテンツチームとして活動した。

カンファレンスの肝になるコンテンツを自分たちで企画する面白さを実感しつつも、「特に重要なトークの採択を1チームだけでやっていいのかな」という思いもあった。スタッフ全体で決めるのがいいのかな。

nao-y.hatenablog.com

趣味

開発

詳しくはPythonタグをば。

Python カテゴリーの記事一覧 - 蛇ノ目の記

OpenStreetMapのデータを使った位置情報の可視化やQRコードに緊急時の情報を埋め込むWebアプリを作ったりしていた。あとはDockerを使ったPythonスクリプトの実行など。広く浅くといった感じ。

写真

Singapore 2018

Shinjuku Night Scene 2018

実は仕事でも写真を撮っていて、PyQオフィシャルブログで連載している「Pythonエンジニア列伝」のnobolisさん回の写真を担当した。

blog.pyq.jp

ポーカー

テキサスホールデムをやっている。

今年はポーカー仲間ができ、主に高円寺のバーで仲間たちとやっている。

上手い人が多くてよくボコられているけど、その割に自分がなかなか成長してない……。

最近、火曜日のまちこ会行けてなくてすいません(´・ω・`)

音楽

音楽自体はずっといろいろ聴いていたけど、音楽ネタのブログを書き始めた。

C95で買った同人音楽の紹介も近いうちに書きたいな。

今年始めたこと: アイドルを聴き始めた - 蛇ノ目の記

MOP of HEAD/空きっ腹に酒/DALLJUB STEP CLUBのライブでぶち上がった - 蛇ノ目の記


Pythonエンジニアとして過ごす初めての1年が経ち、仕事でPythonを書くことにも慣れてきたけど、「〜完全に理解した」とか言う間もなく「〜ぜんぜんわからない」になるくらいには苦戦しながらやっている。主にDjangoとnginx。Web系なのにWebに弱い(´・ω・`) 経験を積んでなんとかしていきたい。愚者なので経験で学ぶ。

なんか去年より振り返り方が雑な気もするけど、ここらで2018年の振り返りを終わりにする。

2018年はありがとうございました。

2019年もよろしくお願いします。

MOP of HEAD/空きっ腹に酒/DALLJUB STEP CLUBのライブでぶち上がった

12/8にDALLJUB STEP CLUBの新アルバムプレリリースライブに行ってきた。

"エモい"と"ぶち上がる"くらいしか音楽の感想を示す語彙を持ってないので、ライブのまとめ 。

MOP of HEADとDALLJUB STEP CLUBは撮影可なので自分でとった映像。

なお、やたら大きく聴こえる歓声はぶち上がってる自分のもの。

MOP of HEAD

2018/12/09 MOP of HEAD "S.A"

2018/12/08 MOP of HEAD "B.O.B"

ドラムの山下さんが中学の先輩だったりする。というのは少し前にライブハウスで話す機会があって知ったこと。世間って狭い。

空きっ腹に酒

www.youtube.com

名前は少し目にしたことがあったけど、聴くのはこのライブが初めてだった。

他の2バンドとは異なり、ラップを基盤としたロックという雰囲気。

DALLJUB STEP CLUB

2018.12.08 DALLJUB STEP CLUB "犬っぽい(Inuppoi)"

WOZNIAKでも活躍する星優太さんがいるバンド。MOP of HEADと同じくクラブミュージック寄りでMOPよりさらにそっちらしい雰囲気。

めっちゃかっこよかった。少し前にsora tob sanakaとツーマンライブやってたけどそれも行ってればよかった…。

DALLJUBの新アルバムのダイジェストはこちら。

www.youtube.com

あとWOZNIAKでお気に入りのHeptagon。

www.youtube.com

今年始めたこと: アイドルを聴き始めた

BeProud Advent Calender 2018 n日目の記事ではありません。

あとシリーズものっぽいタイトルだけど続きません。

アイドルといってもメジャーなのは聴いてないので、推し紹介みたいな趣き。

この間のBPLLでの自己紹介のお題が「今年始めたこと」だった。

bpstudy.connpass.com

そのときは「区立体育館の激安ジムで体を動かし始めた」と喋ったんだけど、アイドルを聴き始めたのも今年からだったことに、今日sora tob sakanaのライブに行ったことで気づいた。

sora tob sakana

sora tob sakanaは平均年齢17歳のアイドルグループ。通称オサカナ。ハイスイノナサsiraphで活動している照井順政さんがプロデュースしている。

siraphについては今年の1月にブログに書いてるので、もしよければそちらもどうぞ。雪山合宿に行ってきた話とライブに行ってきた話 - 蛇ノ目の記

オサカナはsiraphが出るイベントで2回くらい観たことがあって、そのときはあまり気に留めていなかった。けれど7月頭に行ったシーシャ屋さんの店内で流れているのを聴いて、唐突にハマることになった。

照井さんの作るポストロックの流れを汲む曲・オサカナのかわいい声が合わさって刺さりまくり、そのままハマりこんだ。ライブで観るとそこに振り付けのかわいさがプラスされるのでヤバい。「ああ無理尊い」と限界オタクと化す。

www.youtube.com

www.youtube.com

他に聴いてるアイドル

ヤなことそっとミュート

www.youtube.com

www.youtube.com

maison book girl

www.youtube.com

www.youtube.com

こうやって見るといわゆるアイドルって感じの曲より、ロックとかポストロック、現代音楽みたいな他のジャンルが濃く出ているのが好きなんだなと思う

Ubuntu 16.04でLet's Encryptを使ってサーバ証明書を取得する

BeProud Advent Calender 2018 15日目の記事です。

adventar.org

仕事でサーバーにSSL証明書を入れる機会があった。

できればワイルドカード証明書使いたい、という気持ちがあった。そこで、手軽に使えてワイルドカード証明書にも対応しているというLet's Encryptを採用。

Let's Encryptのクライアントならワイルドカード証明書が取得できる、そう思っていた時期がありました(´・ω・`)

環境

tl;dr

certbotではなくletsencrypt

Let's Encryptユーザーガイドを見ると、certbotコマンドを使っているが、Ubuntu 16.04ではletsencryptコマンドを使う。

インストール方法: Ubuntu 16.04 (xenial)に従って、

sudo apt-get install letsencrypt python-letsencrypt-apache

とするだけでいい。apacheプラグインが不要な場合は python-letsencrypt-apacheは省略する。

Ubuntu 16.04にインストールする方法はあるが、リポジトリを追加する必要がある。

www.digitalocean.com

オーケストレーションの際にリポジトリを追加する手間が増えるので、今回は採用しなかった。ワイルドカード証明書は必須の要件ではなかったし。

ワイルドカード証明書

ワイルド証明書とは、一枚の証明書で同じドメインに属しているサブドメインSSL化ができる証明書のこと。

証明書を取得する際に、*.example.comのようにドメイン名の左側にアスタリスクをつける。

参考: ワイルドカード証明書について | JPRS

ワイルドカード証明書を取得するには、Let's EncryptのパッケージがACME v2 APIに対応していなければいけない。ACMEとは証明書発行のプロトコルのこと。

参考: Let's Encrypt を支える ACME プロトコル - Block Rockin’ Codes

Let's Encryptでのワイルドカード証明書取得については以下が詳しい。

ACME v2 とワイルドカード証明書の技術情報 - Let's Encrypt 総合ポータル

ワイルドカード証明書と ACME v2 へ対応 - Let's Encrypt 総合ポータル

ACME v2 APIに対応したクライアントは、ACME Client Implementations - Let's Encrypt - Free SSL/TLS Certificatesで一覧できる。そしてここにletsencryptの名前はない

サーバ証明書の取得

Webサーバが動作している環境では、webrootプラグインを使う。

$ letsencrypt certonly --webroot {webroot-path} -d certificated.example.com

{webroot-path}はWebサーバのドキュメントルートディレクトリ。

Webアプリが/home/www/myapp/であれば、{webroot-path}/home/www/myapp/になる。

このコマンドを実行すると{webroot-path}/.well-known/acme-challengeにワンタイムトークンが作られる。Let's Encryptの認証サーバーがexample.com/.well-known/acme-challenge/{token}に対してHTTPリクエストすることで、ドメインの認証を行う仕組みになっている。

今回の案件ではドキュメントルートディレクトリではなく/home/www/letencrypt/を指定した。

この場合、HTTP通信(80番ポート)を通したcertificated.example.comへのリクエストに対して、/home/www/letencrypt/マッピングしておく。

# /etc/nginx/nginx.conf
server {
    listen 80;
    server_name certificated.example.com;

    location / {
        root /home/www/letsencrypt;
    }
}

コマンドを実行するとどうなる?

過去に証明書を取得したことがない場合は、鍵の紛失時の連絡や期限切れ通知に使うメールアドレスを入力するTUIが起動する。メールアドレスを入力すると利用規約への同意が求められる。これも初回のみ。

nginxにSSL通信の設定をする

サーバ証明書が取得できたので、nginxの設定をしていく。

サーバ証明書秘密鍵/etc/letsencrypt/live/{ドメイン名}に置かれている。

# /etc/nginx/nginx.conf
server {
    listen 443;
 server_name certificated.example.com
    ssl on;
    ssl_certificate /etc/letsencrypt/live/certificated.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/certificated.example.com/privkey.pem;

    location / {
    . . .
    }
}

server {
    listen 80;
    server_name certificated.example.com;

    location / {
        root /home/www/letsencrypt;
    }
}

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