入れ子のデータ — 辞書のリスト
本物のデータが届く形:リストの中の辞書、辞書の中のリスト。中に入り込む方法、グルーピング、そしてここにcopy()だけでは足りない理由。
どうやって学びますか?
解決する問題
この数章では4つのコンテナを学びました — リスト、タプル、辞書、セット。どの例もそのどれかを1つ使い、深さは1段だけでした。
本物のデータは、そんなふうには届きません。注文のリストはこう見えます:
customer item quantity price
rafi pen 3 15.0
ahmed bag 1 850.0ここには2つの段があります。外側は集まりです — 注文の数は分からないのでリスト。内側の各注文は、部分にすべて名前が付いたレコードです — だから辞書です。
APIから返ってくるもの、CSVを読んで得るもの、データベースが返すもの:その形はほとんどいつもこうです — 辞書のリスト。 今日の章で新しいコンテナは登場しません。持っているものを入れ子にすることと、そこから生じる間違いを見分けることがテーマです。
この章を終える頃には
- 辞書のリストの中に入り、それを頭からたどれる
- 奥深くの
KeyErrorやIndexErrorを読んで、どの段で間違えたか判断できる - 辞書のリストをグルーピングして、要約できる
.get()のデフォルト値で、欠けたレコードを扱える- 入れ子のデータに
copy()だけでは足りない理由を説明できる
前提条件: セット — 重複のない集まり.
形を見分ける
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
]
print(len(orders))
print(orders[0])
print(orders[0]["customer"])
print(orders[1]["price"])2
{'customer': 'rafi', 'item': 'pen', 'quantity': 3, 'price': 15.0}
rafi
850.0orders[0]["customer"]という行が、この章の中心的なスキルです。読み方は左から右へ、1段ずつです:
orders— リストorders[0]— その1つ目の要素で、これは辞書orders[0]["customer"]— その辞書のcustomerの値
迷ったら、止まって中間の段を表示しましょう。辞書であることを目で確かめるためにprint(orders[0])と書く — その習慣ひとつで、入れ子のデータのトラブルは半分消えます。
ループでたどる
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
]
for order in orders:
total = order["quantity"] * order["price"]
print(f"{order['customer']:<8} {order['item']:<6} {total:>8.2f}")rafi pen 45.00
ahmed bag 850.00for order in ordersは、1周ごとに辞書を渡し、その中では名前で値にたどり着きます。インデックスの計算はどこにもありません。
f-stringの中のクォート。 f"{order['customer']}"が中でシングルクォートを使っているのは、f-string自体がダブルクォートで書かれているからです。内外で同じ種類を使うと、Pythonはテキストがどこで終わるか判別できません。内側と外側で別々の種類にしましょう。欠けたレコード
本物のデータでは、すべてのレコードがすべてのキーを持つわけではありません:
orders = [{"customer": "rafi"}]
print(orders[0]["discount"])KeyError: 'discount'KeyErrorはキーがないことを伝えますが、どのレコードが欠けているまでは教えてくれません。1万2千件の注文の中で1回だけそうなると、見つけるのは苦痛です。enumerateでたどると各レコードの番号が手に入るので、キーのないレコードを表示できます:
orders = [
{"customer": "rafi", "discount": 5},
{"customer": "ahmed", "discount": 0},
{"customer": "dia"},
]
for i, order in enumerate(orders):
if "discount" not in order:
print("record", i, "has no discount:", order)record 2 has no discount: {'customer': 'dia'}これで問題はレコード2にあり(数え方はゼロから)、そのレコードがどんなものかも分かります。
キーがないことが正常なら、.get()です:
orders = [{"customer": "rafi"}]
print(orders[0].get("discount", 0))0第15章のルールもここに当てはまります:あるべきときこそ角括弧、ないことが正常なら.get()。 入れ子のデータでは、この誘いがより強くなります — .get()はあらゆるエラーを黙らせてしまうからです。そして欠けたデータは黙ってゼロにされ、計算に入ってしまいます。
もう一つの形 — 辞書の中のリスト
by_customer = {
"rafi": ["pen", "ink"],
"ahmed": ["bag"],
}
by_customer["rafi"].append("bottle")
by_customer["dia"] = ["eraser"]
print(by_customer)
for customer, items in by_customer.items():
print(f"{customer}: {len(items)} item(s) - {', '.join(items)}"){'rafi': ['pen', 'ink', 'bottle'], 'ahmed': ['bag'], 'dia': ['eraser']}
rafi: 3 item(s) - pen, ink, bottle
ahmed: 1 item(s) - bag
dia: 1 item(s) - eraserby_customer["rafi"].append("bottle")という行は、2段を1度にやっています:by_customer["rafi"]がリストを取り出し、続いてappendがそのリストに追加します。
この形は第15章の問題に答えます。そこではキーは1回しか存在できなかったので、1人の人に複数のレコードを持てませんでした。値をリストにすれば、持てるようになります。
グルーピング — いちばん使えるパターン
辞書のリストを「誰が何を買ったか」に変換します:
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "ahmed", "item": "bag"},
{"customer": "rafi", "item": "ink"},
]
grouped = {}
for order in orders:
customer = order["customer"]
if customer not in grouped:
grouped[customer] = []
grouped[customer].append(order["item"])
print(grouped){'rafi': ['pen', 'ink'], 'ahmed': ['bag']}真ん中の3行がこのテクニックのすべてです:キーがなければ、先に空のリストを置いてから、心配なく追加する。 それがないと、最初の注文でKeyErrorが出ます。
同じことを1行に収めることもできます:
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "rafi", "item": "ink"},
]
grouped = {}
for order in orders:
grouped.setdefault(order["customer"], []).append(order["item"])
print(grouped){'rafi': ['pen', 'ink']}setdefault(key, [])は「このキーの値をくれ。まだなければこの空のリストをそこに置いて、それをくれ」と言います — 返ってきたものに追加していくのです。.get()との違いが大事です:.get()は辞書に何も入れませんが、setdefaultは入れます。
3行の書き方は読みやすく、1行の書き方は書くのが速いです。実際のコードでは、両方に出会います。
さらに深いところへ
shop = {
"name": "Corner Store",
"staff": [
{"name": "rafi", "shifts": ["mon", "tue"]},
{"name": "dia", "shifts": ["wed"]},
],
}
print(shop["staff"][0]["shifts"][1])
print(len(shop["staff"]))
for person in shop["staff"]:
print(f"{person['name']}: {len(person['shifts'])} shift(s)")tue
2
rafi: 2 shift(s)
dia: 1 shift(s)shop["staff"][0]["shifts"][1]は4段です:辞書 → リスト → 辞書 → リスト。左から右に読めば、難しいことはなく、長いだけです。
3段を超えたら、止まって考えましょう。連鎖を切って中間の段に名前を付けると、コードが読みやすくなります:
first_person = shop["staff"][0]
print(first_person["shifts"][1])入れ子のデータにcopy()だけでは足りない
第12章では、b = aが1つのリストに2つの名前を残すこと、そしてcopy()がその対策であることを確かめました。入れ子のデータでは、copy()は1段分しか守りません:
import copy
template = {"name": "", "items": []}
a = template.copy()
b = copy.deepcopy(template)
a["items"].append("pen")
b["items"].append("bag")
print(template)
print(a)
print(b){'name': '', 'items': ['pen']}
{'name': '', 'items': ['pen']}
{'name': '', 'items': ['bag']}aはcopy()で作りました — 新しい外側の辞書ですが、内のリストは同じリストです。だからa["items"].append("pen")は、元のtemplateも変えてしまいました。
bはdeepcopy()で作りました。中のものもすべてコピーするので、bへの変更はどこにも波及しません。
copyはモジュールなので、使う前にimport copyが必要です — モジュールの本格的な登場は第22章です。
deepcopyはいつ必要ですか? 入れ子の構造のコピーを取って、そのコピーを変えたいのに、元のほうには触れてほしくないときです。deepcopyは遅いこと、そしてほとんどの場合は不要なことに注意しましょう — ほとんどのコードは、既存のデータを変更するのではなく、新しいデータを作っているからです。
完全な例
orders.py:
# The shape data actually arrives in: a list of records
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
{"customer": "rafi", "item": "ink", "quantity": 2, "price": 120.0},
{"customer": "dia", "item": "pen", "quantity": 5, "price": 15.0},
]
print("Customer Item Qty Total")
grand_total = 0.0
for order in orders:
total = order["quantity"] * order["price"]
grand_total = grand_total + total
print(f"{order['customer']:<9} {order['item']:<6} {order['quantity']:>3} {total:>10.2f}")
print(f"{'':<20}{grand_total:>10.2f}")
print()
# Group the spend by customer: the key may not exist yet, so .get supplies a start
spend = {}
for order in orders:
customer = order["customer"]
spend[customer] = spend.get(customer, 0.0) + order["quantity"] * order["price"]
# Sort by amount by putting the amount first in a tuple
pairs = []
for customer, amount in spend.items():
pairs.append((amount, customer))
pairs.sort(reverse=True)
print("Spend per customer, highest first:")
for amount, customer in pairs:
print(f" {customer:<8} {amount:>8.2f}")
distinct = set()
for order in orders:
distinct.add(order["item"])
print()
print("Distinct items:", sorted(distinct))
print("Best customer :", pairs[0][1])Customer Item Qty Total
rafi pen 3 45.00
ahmed bag 1 850.00
rafi ink 2 240.00
dia pen 5 75.00
1210.00
Spend per customer, highest first:
ahmed 850.00
rafi 285.00
dia 75.00
Distinct items: ['bag', 'ink', 'pen']
Best customer : ahmedこの小さなプログラムは、この数章で学んだ4つのコンテナをすべて、それぞれが合う場所で使っています。
リストは注文に使います。数は分からないし、順序も大事だからです。
辞書は各注文に使います。部分にすべて名前が必要だからです。そしてspendにも使います — 名前ごとに集計していくからです。
タプルはpairsの各ペアに使います。ここにはワザがあります:金額をタプルの先頭に置くのです。Pythonはタプルを先頭の値でソートするからです。だから単純な.sort(reverse=True)で金額順になり、key=は不要です。
セットはdistinctに使います。質問は「何種類か」で、penは2回現れるからです。
そしてspend.get(customer, 0.0)は、第15章の数えるパターンのもう一つの顔です — 1を足す代わりに金額を足しています。
うまくいかないとき
KeyError: 'discount' — でも、どのレコード? for i, order in enumerate(orders):でたどっておけば、失敗したときにiが手元にあります — 上の例のように。あるいはループの中にprint(order)を入れましょう。最後に表示されたものが犯人です。
TypeError: string indices must be integers ほぼ毎回、辞書だと思っているものが文字列だという意味です。ordersが辞書のリストではなく名前のリストなら、order["customer"]はまさにこのエラーを出します。print(type(order))で確認しましょう。
TypeError: list indices must be integers or slices, not str 真逆の間違いです — リストに対して名前で聞いています。たいてい[0]が抜けているのです:orders["customer"]ではなくorders[0]["customer"]と書きます。
AttributeError: 'list' object has no attribute 'get' 同じ種類の間違いが、別の形で現れたものです。.get()を呼んだ対象は、辞書ではなくリストです。
コピーを変えたら、元のほうも変わった copy()は浅いコピーです — 内のリストと辞書は共有されたままです。import copyして、copy.deepcopy(...)を使いましょう。
グルーピングの最初の1周目でKeyErrorが出た キーが存在する前にgrouped[key].append(...)が動いていました。if key not in grouped:で空のリストを作ってからにするか、setdefaultを使いましょう。
ステップ 4 / 6 — 予想する
理解度をチェック
2段入れ子になっています。何が表示されるでしょう?
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "ahmed", "item": "bag"},
]
print(orders[1]["item"])- Apen
- Bbag
- Cahmed
- D`KeyError`
グルーピングを試みています。最初の1周目で何が起きるでしょう?
orders = [
{"customer": "rafi", "item": "pen"},
]
grouped = {}
for order in orders:
grouped[order["customer"]].append(order["item"])
print(grouped)- A`AttributeError` — 辞書には`append`がない
- B`KeyError: 'rafi'` — キーがまだ存在しなので、追加先がない
- C`{}`が表示される
- D動いて`{'rafi': ['pen']}`が表示される
copy()を取り、それを2箇所変えました。元のほうはどうなったでしょう?
import copy
template = {"name": "", "items": []}
a = template.copy()
a["name"] = "rafi"
a["items"].append("pen")
print(template)- A{'name': '', 'items': ['pen']}
- B{'name': '', 'items': []}
- C{'name': 'rafi', 'items': ['pen']}
- D{'name': 'rafi', 'items': []}
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
library.pyというファイルを書き、6冊以上の本のリストを保存しましょう。各本は、タイトル・著者・出版年・冊数を持つ辞書です。
そして:
- すべての本を列を揃えて表示する
- 総冊数と平均の出版年を求める
- 著者ごとに
{"author": ["title", "title"]}の形でグルーピングし、各著者が何冊持つかを表示する - セットで異なる著者の数を数える
- 最も冊数の多い本のタイトルを表示する
次に、本物のデータがするように、それを壊しましょう:
- 1冊から
"year"キーをまるごと削除して、プログラムを実行します。どの行で止まりましたか? そのメッセージは、どの本に問題があったか教えてくれましたか? - その行で
.get("year", 0)を使い、もう一度実行します。今度の平均の出版年は? それは正しいですか?
最後の質問こそが、本当の学びです。.get()はエラーを止めましたが、ない年をゼロとして平均してしまうのは、黙って間違った答えにたどり着くことです。エラーを消すことと、問題を解くことは同じではありません。