リストは番号で取り出す。ここまでは理解できたのに、辞書が出てきたとたんに手が止まった経験はありませんか。
波かっこ、コロン、キー。見慣れない記号が一度に増えるので、身構えてしまうのも無理はありません。
ところが辞書の考え方そのものは、とても素朴です。番号ではなく名前でしまう。ただそれだけの入れ物です。
そして実務のPythonでは、辞書を触らない日がありません。設定ファイル、APIの返事、集計の途中経過。気づけばいつも辞書が中心にいます。
この記事では、辞書の基本と落とし穴を、手元で動かせるコードと一緒に整理していきます。
辞書は、名前をつけてしまう入れ物¶
まずは形から確認しましょう。辞書は、キーと値を組にして持つデータ構造です。
キーが名札で、値が中身です。名札さえわかっていれば、何番目かを気にせず取り出せます。
言葉より動かしたほうが早いので、次のコードを写してみてください。
user = {"name": "佐藤", "age": 28, "city": "東京"}
print(user["name"]) # 佐藤
print(len(user)) # 3
print("age" in user) # True
角かっこにキーを書くだけで、対応する値が返ってきました。user[0] のような番号は使いません。
3行目の in にも注目してください。辞書に対する in は、キーがあるかどうかを調べます。値ではありません。
リストとの違いを表にまとめておきます。どちらを使うか迷ったら、ここに戻ってきてください。
| list | dict | |
|---|---|---|
| 取り出し方 | 番号(x[0]) |
キー(x["name"]) |
| 中身の形 | 値だけ | キーと値の組 |
| 重複 | 値は重複できる | キーは重複できない |
| 向いている場面 | 順番に並んだデータ | 名前で引きたいデータ |
リストとタプルの違いから整理したい方は、先にこちらを読んでおくと全体像がつかみやすくなります。【関連記事】Pythonのリスト(list)とタプル(tuple)、どっちを使う? それぞれの違いを徹底解説
取り出し方は2つある。getを覚えると事故が減る¶
ここが初心者と実務者の分かれ目になるところです。値の取り出し方には角かっこと get の2種類があります。
違いは、キーが無かったときの振る舞いです。角かっこはエラーで止まり、get は代わりの値を返します。
user = {"name": "佐藤", "age": 28}
print(user.get("email")) # None
print(user.get("email", "未登録")) # 未登録
print(user["email"]) # KeyError: 'email'
2行目のように、get には第2引数で初期値を渡せます。キーが無ければその値が返るので、処理を止めずに先へ進めます。
私は10年ほどエンジニアとして開発に関わってきましたが、この使い分けで一度痛い目を見ました。外部APIの返事を data["items"] で受け取っていたところ、該当データが無い日だけ items ごと省略される仕様で、夜間バッチが落ちたのです。
以来、自分の管理下にないデータは get で受けると決めています。キーの存在を保証できるかどうかが、判断の分かれ目です。
KeyErrorそのものの読み方を確認したい方は、こちらで詳しく扱っています。【関連記事】PythonでKeyErrorが出る理由とは?辞書dictで初心者がハマるポイント
初期値を入れてから使いたいときは setdefault¶
get には弱点があります。値を返すだけで、辞書そのものは変わりません。
空のリストを用意してから追加したい場面では、setdefault が便利です。キーが無ければ初期値を入れ、そのうえで値を返してくれます。
members = [("営業", "佐藤"), ("開発", "遠藤"), ("営業", "加藤")]
teams = {}
for dept, name in members:
teams.setdefault(dept, []).append(name)
print(teams)
# {'営業': ['佐藤', '加藤'], '開発': ['遠藤']}
if でキーの有無を確かめる行が消えました。データをグループ分けするとき、この書き方がそのまま使えます。
取り出し方の違いを表にしておきます。迷ったらこの4行で判断できます。
| 書き方 | キーがあるとき | キーが無いとき |
|---|---|---|
d["key"] |
値を返す | KeyErrorで止まる |
d.get("key") |
値を返す | Noneを返す |
d.get("key", 0) |
値を返す | 0を返す(辞書は変わらない) |
d.setdefault("key", []) |
値を返す | 空リストを入れてから返す |
ループの回し方は3種類ある¶
辞書をそのままforに渡すと何が出てくるか、意外と自信を持って答えられない方が多い部分です。
答えはキーです。値ではありません。
scores = {"python": 80, "sql": 65, "git": 90}
for key in scores:
print(key) # python, sql, git
for key, value in scores.items():
print(key, value) # python 80 ...
print(sum(scores.values())) # 235
キーと値を両方使いたいなら items() を呼びます。値だけでよければ values() です。
items() が返すのはビューと呼ばれる特別な入れ物で、元の辞書と連動しています。リストのコピーではないので、大きな辞書でも余計なメモリを使いません。
ひとつ注意点があります。ループの最中にキーを追加したり削除したりすると、実行時エラーになります。
消したいキーがあるなら、先に候補を集めてから消すか、list(scores) のようにキーの一覧をコピーしてから回してください。
追加、更新、削除の書き方¶
辞書は作ったあとから自由に変えられます。追加も更新も、同じ書き方で済みます。
存在しないキーに代入すれば追加、存在するキーに代入すれば上書きです。
base = {"color": "blue", "size": "M"}
base["stock"] = 5 # 追加
base["size"] = "L" # 上書き
base.update({"color": "red"})
print(base)
# {'color': 'red', 'size': 'L', 'stock': 5}
print(base.pop("stock")) # 5 ← 値を受け取りながら消せる
del base["color"]
print(base) # {'size': 'L'}
update() は、別の辞書の内容をまとめて反映するメソッドです。設定のデフォルト値に利用者の指定を重ねる、といった場面でよく使います。
Python 3.9からは、辞書どうしを縦棒でつなぐ書き方も加わりました。PEP 584で導入された結合演算子です。
defaults = {"color": "blue", "size": "M"}
custom = {"size": "L"}
merged = defaults | custom
print(merged) # {'color': 'blue', 'size': 'L'}
print(defaults) # {'color': 'blue', 'size': 'M'} ← 元は無事
update() との違いは、元の辞書を壊さないことです。新しい辞書が返るので、設定を組み立てる処理が読みやすくなります。
同じキーがぶつかったときは、右側の値が勝ちます。上書きの向きだけ覚えておけば迷いません。
キーにできる型、できない型¶
辞書のキーには何でも置けるわけではありません。ここに小さな制限があります。
辞書は、キーの値そのものから保管場所を計算しています。だから途中で中身が変わる型は使えません。
ok = {("東京", "港区"): 1, 3: "three"}
print(ok[("東京", "港区")]) # 1
try:
{["東京", "港区"]: 1}
except TypeError as e:
print(e)
# unhashable type: 'list'
文字列、数値、タプルはキーにできます。リストや辞書はキーにできません。
unhashable という単語が出たら、変更できる型をキーにしようとしたと考えてください。座標や複合キーを扱いたいときは、リストではなくタプルにするだけで解決します。
この制限は集合(set)とまったく同じ理屈です。仕組みから理解したい方はこちらが参考になります。【関連記事】Pythonの集合(set)とは?重複の削除と高速な存在チェックを初心者向けに解説
並び順は保たれる。ただし、いつからかを知っておく¶
昔のPython入門書には、辞書に順番はないと書かれています。今の説明と食い違うので、混乱する方が多い部分です。
結論から言うと、現在の辞書は書いた順を保ちます。CPython 3.6で実装として順序が保たれるようになり、Python 3.7で言語仕様として正式に保証されました。
つまり3.7以降を使っているなら、items() で回したときの順番は、キーを入れた順です。JSONに書き出したときの並びも予測できます。
私が新人のころは、この保証がありませんでした。辞書の順に依存したテストが環境によって落ちるという、原因のわかりにくい不具合を何度か見ています。
順番を使った小技¶
順序が保証されると、辞書は重複除去の道具にもなります。dict.fromkeys() を使う書き方です。
visitors = ["sato", "endo", "sato", "kato", "endo"]
print(list(dict.fromkeys(visitors)))
# ['sato', 'endo', 'kato'] ← 最初に出てきた順のまま
キーは重複できないので、結果として重複が落ちます。set() で包むより、並び順が保たれるぶん扱いやすい場面があります。
実務でよく書く形と、つまずきポイント¶
最後に、現場でそのまま出てくる書き方を紹介します。まずは辞書内包表記です。
リスト内包表記と同じ発想で、キーと値をコロンでつないで書きます。
prices = {"apple": 120, "banana": 80, "melon": 900}
expensive = {name: price for name, price in prices.items() if price >= 100}
print(expensive) # {'apple': 120, 'melon': 900}
flipped = {price: name for name, price in prices.items()}
print(flipped) # {120: 'apple', 80: 'banana', 900: 'melon'}
条件で絞り込む処理が1行に収まりました。空の辞書を作ってループで詰めるより、意図が伝わりやすくなります。
APIやJSONを扱うときも、届いたデータはほぼ辞書の形です。読み書きの手順はこちらでまとめています。【関連記事】PythonでJSONデータの扱いをマスター!API連携に必須のjsonモジュールの使い方
つまずきやすい点を表にしておきます。どれも一度知っておけば避けられるものです。
| つまずきポイント | 起きること | 対処 |
|---|---|---|
| 無いキーを角かっこで取る | KeyErrorで止まる | get に初期値を渡す |
| ループ中にキーを消す | 実行時エラーになる | キーの一覧をコピーしてから回す |
| リストをキーにする | unhashable でエラー | タプルにする |
値の有無を in で調べる |
キーしか見ていない | in d.values() と書く |
| 同じキーを2回書く | あとに書いた値だけ残る | 重複していないか確認する |
集計や初期値の準備が毎回めんどうだと感じたら、標準ライブラリに便利な辞書が用意されています。defaultdictやCounterがそれです。【関連記事】Pythonのcollectionsとは?Counterやdefaultdictで毎日のコードが短くなる使い方を解説
今日から使える形にまとめておく¶
ここまでの内容を、実際に書くときの判断だけに絞って整理します。
自分で作った辞書は角かっこでよい。外から来たデータは get で受ける、という切り分けで事故はほとんど防げます。
キーと値を両方使うループでは items() を呼ぶ。値の合計や最大値がほしいなら values() を渡すだけで済みます。
グループ分けをしたくなったら setdefault を思い出してください。if を書く前に手が動くようになれば、辞書はかなり自分のものになっています。
辞書は、Pythonの中でいちばん出番の多いデータ構造です。 焦って全部を覚える必要はありません。
まずは get と items() の2つだけ、手元のコードで使ってみてください。それだけでも、エラーで止まる回数がはっきり減るはずです。
ここまでお読みいただきありがとうございました。