辞書 — キーと値のペア
名前で値を保存して取り出す、`.get()`で`KeyError`を避ける、キーの追加・変更・削除、`.items()`での走査、そしてリストがキーになれない理由。
どうやって学びますか?
解決する問題
3人の点数を保存する必要があります — どの点数が誰のものか、それが分かるように。
リストでできるのは1通りだけです:並べた2つのリスト。
names = ["rafi", "ahmed", "bilal"]
marks = [72, 45, 90]
print(names[1], marks[1])ahmed 45動きます。そしてその並べ方はあなた自身の注意力だけで成り立っていて、Pythonの助けはまったくありません。
namesに人を足してmarksを忘れたら?2つのリストの長さが違って、そこからnames[2]とmarks[2]は別の2人を指すようになります。Pythonは一言も言いません。namesをソートしたら?関係は完全に壊れ、しかも黙って壊れます。
根本はここです:2つのものの関係が、どこにも書き留められていません。関係は「同じインデックス」というだけ — いつでも壊れ得る偶然にすぎません。
辞書は、その関係をコードの中に書き込みます。インデックスの代わりに名前を使い、その組を1か所にまとめて持ちます。
この章を終える頃には
- 辞書を作り、キーで値を取り出せる
.get()でKeyErrorを避け、それぞれが適切なのはいつか分かる- キーの追加・変更・削除ができる
.keys()、.values()、.items()で辞書を走査できる- リストがキーになれないのにタプルはなれる理由を説明できる
前提条件: タプルとアンパッキング.
辞書 — キーと値のペア
中括弧の中に、key: valueのペアを入れます:
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
print(marks)
print(marks["ahmed"])
print(len(marks)){'rafi': 72, 'ahmed': 45, 'bilal': 90}
45
3marks["ahmed"] — インデックスではなく名前です。しかもその名前はコードの中にあり、半年後に読んでも、何を要求しているのかが分かります。
len()はペアを数えます:3つ。
順序について一言。 Python 3.7以降、辞書は入れた順序を覚え、その順に表示します。ただし辞書は位置ではなく名前で調べるものです —marks[0]と書くと、Pythonは数のゼロというキーを探しますが見つからず、KeyError: 0で止まります。
存在しないキー — KeyErrorと.get()
marks = {"rafi": 72}
print(marks["dia"])KeyError: 'dia'KeyErrorは「その名前のキーがない」という意味です。同じ間違いをリストでするとIndexErrorが出ました — 同じ種類のエラーに対する、違う名前です。
止まる代わりに安全な答えを得るには、.get()を使います:
marks = {"rafi": 72}
print(marks.get("dia"))
print(marks.get("dia", 0))
print(marks.get("rafi", 0))None
0
722つ目の引数は「なかったらこれをくれ」という意味です。与えないとNoneが返ります。
どっちを、いつ使う? キーが本来あるべきものならmarks["rafi"]と書きます — ないことはバグなので、KeyErrorがすぐにお知らせしてくれます。キーがないことが正常なら.get()を使います。どこでも.get()に頼ると、悪いデータが黙ってすり抜けます。それこそ見つけたかった悪いデータなのに。
追加・変更・削除
marks = {"rafi": 72}
marks["dia"] = 88
marks["rafi"] = 75
print(marks)
del marks["rafi"]
print(marks){'rafi': 75, 'dia': 88}
{'dia': 88}同じ行が2つの仕事をします:キーがなければ追加し、あれば置き換える。専用の「追加」メソッドはありません。
そこから重要なルールが生まれます — キーは1回しか現れません:
marks = {"rafi": 72, "rafi": 90}
print(marks){'rafi': 90}エラーはありません。2つ目が1つ目を黙って消しました。これはファイルからデータを読み込むとき日常的に起きる話で、記録が消えるよくある原因です。
inはキーを検索し、値は検索しない
marks = {"rafi": 72, "ahmed": 45}
print("rafi" in marks)
print(72 in marks)True
False72は確かに値としてはありますが、inが見るのはキーだけです。値を検索したいなら、そう伝える必要があります:72 in marks.values()。
走査する3つの方法
marks = {"rafi": 72, "ahmed": 45}
for name in marks:
print(name)
print("---")
for name, mark in marks.items():
print(name, mark)
print("---")
print(list(marks.keys()))
print(list(marks.values()))rafi
ahmed
---
rafi 72
ahmed 45
---
['rafi', 'ahmed']
[72, 45]1つ目は注目に値します:for name in marksが返すのはキーで、値ではありません。初めて見ると誰でも驚きます。
両方欲しいときは、.items()がそのつどタプルを返します — それをname, markがアンパッキングします。前章で学んだアンパッキングが役を発揮するのはここです。
値に対して計算をするときは.values():
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
print(sum(marks.values()))
print(max(marks.values()))
print(round(sum(marks.values()) / len(marks), 2))207
90
69.0何がキーになれるのか
キーになれるのは変更できないものだけです。テキスト、数、タプルはどれもできます。リストはできません:
d = {}
d[["a", "b"]] = 1TypeError: unhashable type: 'list'「ハッシュできない(unhashable)」という言葉は、分解して考える価値があります。辞書がこれほど速く探せるのは、各キーから数を計算して、その数で正しい場所にまっすぐ行くからです。後からキーが変わり得たら、その数は間違ってしまい、エントリは失われます。ですからPythonは、変更できるものをキーにすること自体を拒否します。
タプルは変わりません。だから優秀なキーになります:
grid = {}
grid[(0, 0)] = "start"
grid[(1, 2)] = "tree"
print(grid)
print(grid[(1, 2)]){(0, 0): 'start', (1, 2): 'tree'}
tree前章で、タプルが変更できないことは利点だと言いました。これがその最初の実証です。
いちばん使えるパターン — 数える
words = ["pen", "bag", "pen", "ink", "pen"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts){'pen': 3, 'bag': 1, 'ink': 1}この行は内側から読みます:counts.get(word, 0)は「この単語の今の数をくれ。まだなければゼロ」と言っています。そこに1を足して、結果を戻します。
ここで働いているのは.get()のデフォルト値です — それがなければ、新しい単語のたびにKeyErrorが出るので、if word in counts:と2つの分岐が必要になります。この1行のパターンは何度も書くことになります。
完全な例
marks.py:
# A dictionary keeps a name attached to its value
marks = {"rafi": 72, "ahmed": 45, "bilal": 90, "dia": 88}
print("Everyone:")
for name, mark in marks.items():
status = "pass" if mark >= 40 else "fail"
print(f" {name:<8} {mark:>3} {status}")
print()
print("Count :", len(marks))
print("Total :", sum(marks.values()))
print("Average :", round(sum(marks.values()) / len(marks), 2))
print("Highest :", max(marks.values()))
best = max(marks, key=marks.get)
print("Best :", best, marks[best])
print("Has dia :", "dia" in marks)
print("Missing :", marks.get("nadia", "not recorded"))
marks["dia"] = 91
marks["nadia"] = 60
del marks["ahmed"]
print()
print("After edits:", marks)Everyone:
rafi 72 pass
ahmed 45 pass
bilal 90 pass
dia 88 pass
Count : 4
Total : 295
Average : 73.75
Highest : 90
Best : bilal 90
Has dia : True
Missing : not recorded
After edits: {'rafi': 72, 'bilal': 90, 'dia': 91, 'nadia': 60}気づく価値のあることが3つあります。
status = "pass" if mark >= 40 else "fail"は条件を1行で書く方法で、真ん中から外側へ読んでいきます:「mark >= 40なら"pass"、そうでなければ"fail"」。第9章の4行のif/elseは、ここでは単一の値を選んでいるだけなので、1行で足ります。本当の判断にはちゃんとifを書きましょう — この形は2つの値から1つを選ぶためだけのものです。
max(marks, key=marks.get)は、第13章のkey=が新しい仕事をしている姿です。(ここでのkey=はmaxの引数の名前で、辞書のキーではありません。)max(marks)なら最大のキー、つまりアルファベット順で最後の名前が返ります。key=marks.getは、代わりに値で各キーを比べるよう指示するので、最も高い点の名前が返ります。点数の最大は簡単(max(marks.values()))なのに、誰の点かを知るにはこの小ワザが必要なことに注目してください。
最後の行の順序。diaはすでにいたので、変わったのは値だけで、位置は変わりません。nadiaは新しいので、末尾に追加されました。ahmedは消えました。辞書は挿入順を覚えていますし、値の置き換えはそれを乱しません。
うまくいかないとき
KeyError: 'dia' その名前のキーがありません。綴りと大文字・小文字を確認しましょう — "Rafi"と"rafi"は別のキーです。ファイルから読んだキーには、よく末尾の空白が付いています。print(list(d.keys()))とすれば、各キーがクォート付きで表示されるので見えます。キーがないことが正常なら.get()を使いましょう。
TypeError: unhashable type: 'list' リストをキーにしようとしたものです。タプルを使いましょう — tuple(my_list)が1つ作ります。
for x in my_dictが値を返してこない 返してこないのが正解です — 返るのはキーだからです。値には.values()、両方には.items()を使いましょう。
2つのキーが同じになって、記録が消えた キーは1回しか存在せず、後のほうが前のを黙って置き換えます。1つの名前で複数の記録を持ちたいときは、値をリストにするのです — それは第17章の題材です。
KeyError: 0 my_dict[0]と書いて、リストであるかのように先頭の要素を要求しています。辞書に位置はありません。0はキーとして検索され、そのようなキーがなければKeyError: 0が出ます。本当に最初のペアが必要なら、list(my_dict.items())[0]と書きます。
辞書に.sort()は使えない その通りです — 辞書には.sort()はありません。sorted(d.items())がソート済みのリストを返します。たいていそれが欲しいものでした。
ステップ 4 / 6 — 予想する
理解度をチェック
ループが辞書を直接走査しています。何が表示されますか?
marks = {"rafi": 72, "ahmed": 45}
for name in marks:
print(name)- Arafi 72 ahmed 45
- Brafi ahmed
- C72 45
- D('rafi', 72) ('ahmed', 45)
存在しないキーを要求しています。何が起きるでしょう?そして安全な代替手段は?
marks = {"rafi": 72}
print(marks["dia"])- A`None`と表示される
- B空行が表示される
- C`KeyError` — 安全な代替手段は`marks.get("dia")`で、`None`が返る
- D`IndexError`
数えるパターンです。何が表示されますか?
words = ["pen", "bag", "pen"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)- A`KeyError`
- B{'pen': 2, 'bag': 1}
- C{'pen': 1, 'bag': 1}
- D{'pen': 3, 'bag': 1}
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
stock.pyというファイルを書き、6品目以上の商品を保持する辞書を作りましょう。キーは商品名、値は価格です。
そして:
- 各商品と価格を列を揃えて表示する
- 合計・平均・最高・最低の価格を表示する
- 最も高い商品の値段だけでなく、名前を探す
- 商品を1つ追加し、価格を1つ変更し、商品を1つ削除してから、もう一度すべて表示する
.get()を使って、存在しない商品を、メッセージをデフォルト値にして問い合わせる
次に数えるプログラムを書きましょう:リストにいくつかの単語を入れ、いくつかは繰り返しておき、counts.get(word, 0) + 1のパターンで、それぞれが何回現れるかを数えます。
最後に意図的に壊しましょう:角括弧で存在しないキーを要求し、リストをキーにしようとしてみます。2つの違うエラーが返ってきます — どれがどれで、なぜそうなのかを自分で説明できるようになりましょう。