Skip to content
This page has been auto-translated and may contain errors.View in English

辞書

リストでは位置によって要素を検索できます。しかし、名前で検索したいことが多くあります。「3番目の要素を取得」ではなく、「ハナコのスコアを取得」のように。辞書はデータをキーと値のペアとして格納します:位置ではなくキーによって値を検索します。

リストの位置インデックスに意味がない場合、辞書が適切な構造です。辞書は任意のキーを値にマッピングし、O(1)時間で名前付き検索ができます。リーダーボード、JSONレスポンス、設定ファイルはすべて、キーと値のマッピングとして自然に表現できます。

dictはハッシュテーブルに基づくキー・バリューストアで、平均O(1)の検索、挿入、削除を実現します。キーはハッシュ可能でなければならず、値は任意のオブジェクトでかまいません。Python 3.7以降、辞書は挿入順を保持します。dictはPythonの名前空間、オブジェクトの__dict__属性、キーワード引数の基盤です。

辞書を作成する

各キーと値の間にコロン、ペアの間にカンマを置き、波括弧で囲みます。キーはほぼ常に文字列です。値は何でもよく、数値、文字列、他のリスト、さらには他の辞書も使えます。

辞書リテラルは波括弧とkey: value構文を使います。キーは任意のイミュータブル(ハッシュ可能)な型、文字列、整数、タプルを使えます。値は任意のPythonオブジェクトでかまいません。辞書は挿入順を保持するので、反復処理すると追加された順に項目が得られます。

辞書リテラルは左から右に評価されます。キーはハッシュ可能でなければなりません: strinttupleは使えますが、listdictは使えません。値に制限はありません。挿入順はPython 3.7時点で保証されています(3.6以降コンパクトハッシュテーブルとして実装)。リテラル内の重複キーは静かに最後の値を使います。

python
player = {
    "name":  "ハナコ",
    "score": 87,
    "level": 5,
    "alive": True,
}

値にアクセスする

キーを使って角括弧で値を取得します。キーが存在しない場合、PythonはKeyErrorを発生させます。キーが存在するかわからない場合は.get()を使います: クラッシュせずNoneを返すか、指定したデフォルト値を返します。

角括弧アクセスは、欠落したキーに対してKeyErrorを発生させます。.get(key)は、ミス時にNoneを返します。.get(key, default)は代わりにデフォルト値を返します。キーの存在が不確かなときは常に.get()を使ってください: try/exceptでアクセスを包むよりも安全で読みやすくなります。

d[key]__getitem__を呼び出し、キーをハッシュ化してテーブルを探索します:平均O(1)。ミス時はKeyErrorを発生させます。.get(key, default=None)は同じ探索を行いますが、ミス時には例外を発生させずにデフォルト値を返します。key in dチェック(__contains__を呼び出す)はO(1)で、アクセスの前のガードとして慣用的な方法です。

python
player = {"name": "ハナコ", "score": 87}

player["name"]    # "ハナコ"
player["score"]   # 87
player["lives"]   # KeyError (キーが存在しない)
python
player.get("score")          # 87
player.get("lives")          # None (エラーなし、デフォルトでNoneを返す)
player.get("lives", 3)       # 3   (キーがない場合のデフォルトを指定)

.get()はキーが欠落している可能性がある場合に安全です:

python
count = inventory.get("arrows", 0)   # "arrows"が辞書にない場合は0

追加と更新

角括弧でキーに代入します。キーがすでに存在する場合、値は置き換えられます。まだ存在しない場合、新しいエントリーが作成されます。.update()を使うと、他の辞書全体を一度にマージできます。

キーへの代入は__setitem__を呼び出します:平均O(1)で、作成または置換します。.update()は他の辞書またはキーと値のペアのイテラブルを受け取り、各エントリーに対して__setitem__を呼び出して既存のキーを上書きします。

d[key] = value__setitem__を呼び出し、キーをハッシュ化してテーブルに挿入または上書きします:平均O(1)。.update(other)__setitem__の繰り返し呼び出しと同等です。|演算子(Python 3.9以降)は、変更せずに辞書をマージし、新しい辞書を返します。|=はインプレースで変更します。

python
player = {"name": "ハナコ", "score": 87}

player["score"] = 92        # 既存を更新
player["level"] = 5         # 新しいキーを追加
python
extras = {"level": 5, "alive": True}
player.update(extras)   # extrasのキーで追加/上書き

項目を削除する

エントリーを削除する4つの方法があります。.pop()はキーを削除し、値を返します。デフォルト付きの.pop()は、キーが存在しないかもしれないときに安全です。delはキーを削除しますが戻り値はありません。.clear()は辞書全体を空にします。

.pop(key)はミス時にKeyErrorを発生させます。.pop(key, default)は代わりにデフォルト値を返すので、安全な削除メソッドになります。del d[key]__delitem__を呼び出し、ミス時にKeyErrorを発生させます。.clear()はすべてのエントリーを削除しますが、辞書オブジェクト自体は保持します。

.pop(key, default)は単一のハッシュ探索です:平均O(1)。del d[key]__delitem__を呼び出し、同じ探索を行い、ミス時に例外を発生させます。削除後、ハッシュテーブルは縮小してメモリを解放することがあります。.clear()はテーブルサイズをリセットします。辞書を反復処理しながら同じループ内で変更すると、RuntimeErrorが発生します。最初に削除するキーのリストを構築してください。

python
player = {"name": "ハナコ", "score": 87, "level": 5}

player.pop("level")            # "level"を削除し、5を返す
player.pop("lives", None)      # 安全なpop、キーがなければNoneを返す
del player["score"]            # "score"を削除、戻り値なし
player.clear()                 # すべてを削除

デフォルト付きの.pop()は、存在しないかもしれないキーを削除する最も安全な方法です。

反復処理

3つのビューを使って辞書のさまざまな部分をループできます。辞書をそのまま反復処理するとキーが得られます。.values()は値を返します。.items()は両方を一度に返し、最もよく使います: 各ペアを2つの名前にアンパックして、きれいで読みやすいループを書けます。

.keys().values().items()ビューオブジェクトを返します。リストではありません。ビューは辞書の現在の状態を動的に反映します:辞書を変更すると、ビューもただちに更新されます。.items()は、ほとんどのループで最も便利です。タプルアンパックfor k, v in d.items()が明快に読めるからです。

.keys().values().items()はそれぞれdict_keysdict_valuesdict_itemsのビューオブジェクトを返します。ビューは遅延評価です:データをコピーせず、基礎となる辞書が変更されると更新されます。dict_keysはキーが一意でハッシュ可能なので、集合演算(&|-)をサポートします。反復処理中に辞書を変更するとRuntimeErrorが発生します。必要ならばlist(d.items())でスナップショットを取ってください。

python
player = {"name": "ハナコ", "score": 87, "level": 5}

for key in player:               # キーを反復処理(最も一般的)
    print(key)

for key in player.keys():        # 同上、明示的なキービュー
    print(key)

for value in player.values():    # 値だけ
    print(value)

for key, value in player.items():   # 両方、最も便利
    print(f"{key}: {value}")

.items()は最もよく使うものです。各ペアを2つの名前にアンパックすることで、ループが読みやすくなります。

メンバーシップをチェックする

inはキーが辞書に存在するかどうかをチェックします。値はチェックせず、キーだけです。何かが存在しないかチェックするにはnot inを使います。

innot in__contains__を呼び出します。辞書ではO(1)です。これはキーのみをチェックします。値をチェックするにはin d.values()を使いますが、値はインデックス化されていないのでO(n)になります。

key in ddict.__contains__を呼び出し、キーをハッシュ化してテーブルを探索します:平均O(1)。value in d.values()は値ビューを反復処理します:O(n)。この非対称性は、値をスキャンするよりも辞書キーで検索することを好む中核的な理由です。

python
player = {"name": "ハナコ", "score": 87}

"name"  in player      # True
"lives" in player      # False
"lives" not in player  # True

inはキーだけをチェックします。値をチェックするにはin player.values()を使いますが、ほとんど必要ありません。

ネストした辞書

値自体が辞書になることもあります。これは、複数のレベルを持つ構造化データを表現する方法です: 統計セクションを持つプレイヤー、サブセクションを持つ設定ファイルなど。2組の角括弧でネストされた値にアクセスします: 最初は外側のキーを選び、2番目は内側のキーを選びます。

ネストした辞書は、値そのものが辞書である辞書です。連鎖した添字でアクセスします。内側の辞書を変更すると外側の辞書にも影響します。外側の辞書は同じオブジェクトへの参照を保持しているからです。可能な限りネストは浅く保ってください: 深くネストすると、すぐに読みにくくなり、ナビゲートしづらくなります。

ネストした辞書はオブジェクトへの参照を格納します。コピーではありません。外側の辞書の浅いコピー(d.copy())は内側の辞書をコピーしません。内側の辞書への変更は、元のオブジェクトとコピーの両方を通じて見えます。深くネストした構造では、copy.deepcopy()が完全に独立したコピーを作成します。連鎖した__getitem__の呼び出しはそれぞれO(1)なので、アクセスの深さに漸近的なコストはありません。

python
users = {
    "はなこ": {"score": 87, "level": 5},
    "たろう": {"score": 74, "level": 3},
}

users["はなこ"]["score"]   # 87
users["たろう"]["level"]   # 3

連鎖した角括弧でアクセスします。深くネストした構造では扱いにくくなるので、できる限りネストは浅く保ってください。

setdefault

.setdefault()はキーが存在すればその値を読み取り、存在しなければデフォルト値を設定して、値を返します。キーを存在させたいが、すでに存在する場合は上書きしたくないときに便利です。

.setdefault(key, default)はアトミックな「読むか作る」操作です: キーが存在すれば現在の値を変更なしに返し、存在しなければデフォルトを挿入して返します。よくある使い方は、別の存在チェックなしにグループ化された構造を構築することです。

.setdefault(key, default)は単一のハッシュ探索です:平均O(1)。キーが存在しない場合、defaultが挿入され返されます。存在する場合、既存の値が返され、defaultは無視されます(チェック後には評価されません)。「項目をリストにグループ化する」よくあるパターンでは、これは追加前にkey in dをチェックする方法の標準的な代替案です。

python
inventory = {}

inventory.setdefault("arrows", 0)    # "arrows": 0を設定、0を返す
inventory.setdefault("arrows", 10)   # "arrows"はすでに存在、変更なし、0を返す

これは、最初にキーの存在をチェックせずにグループ化された構造を構築するのに便利です:

python
groups = {}

for name, team in players:
    groups.setdefault(team, []).append(name)

collections.defaultdictとCounter

標準ライブラリには、よくあるパターンを自動的に処理する2つの辞書サブクラスがあります。defaultdictは欠落したキーに対してデフォルト値を作成するので、KeyErrorが発生しません。Counterはシーケンス内で各項目が何度現れるかを数え、結果を辞書として返します。

defaultdictは新しいキーのデフォルト値を生成する呼び出し可能オブジェクトを受け取り、.setdefault()の必要を排除します。Counterは頻度カウント用に特化した辞書で、.most_common()メソッドを持ちます。両者とも辞書のサブクラスなので、すべての標準的な辞書操作が機能します。

defaultdict.__missing__はファクトリを呼び出し結果を保存するので、よくあるケースではスレッドセーフです。Counterdictのサブクラスで、.most_common(n)(heapq経由でO(n log n))、.subtract()、カウントの組み合わせのための算術演算子を追加します。両者はcollectionsにあります。インポートはモジュールの章で扱います。

collectionsのインポート

defaultdictCounterは標準ライブラリからインポートする必要があります。インポートはモジュールの章で扱います。

python
from collections import defaultdict

groups = defaultdict(list)
for name, team in players:
    groups[team].append(name)   # teamが新しくてもKeyErrorなし
python
from collections import Counter

words  = ["cat", "dog", "cat", "bird", "cat", "dog"]
counts = Counter(words)
# Counter({'cat': 3, 'dog': 2, 'bird': 1})

counts.most_common(2)   # [('cat', 3), ('dog', 2)]

Counterは、「ループで物を数える」決まり文句を多く節約してくれます。

実践例

スコアトラッカーを構築し、すべてのエントリーを含むサマリーを表示する例:

python
scores = {"ハナコ": 87, "タロウ": 74, "サクラ": 92, "ケンタ": 55}

total   = sum(scores.values())
average = total / len(scores)

print(f"Players:  {len(scores)}")
print(f"Average:  {average:.1f}")
print(f"Highest:  {max(scores.values())}")
print(f"Lowest:   {min(scores.values())}")
print()

for name, score in scores.items():
    print(f"  {name}: {score}")

ループでファイルごとの結果の辞書を構築し、すべてのエントリーにわたって要約する例:

python
job_results = {}
files       = ["report_jan.csv", "report_feb.csv", "report_mar.csv"]

for filename in files:
    size = len(filename) * 100   # 実際のファイルサイズの代わり
    if size < 2000:
        status = "ok"
    else:
        status = "large"
    job_results[filename] = {"size": size, "status": status}

ok_count    = 0
large_count = 0

for result in job_results.values():
    if result["status"] == "ok":
        ok_count += 1
    else:
        large_count += 1

print(f"Processed {len(job_results)} file(s): {ok_count} ok, {large_count} large")

ネストしたリクエスト辞書を必須フィールドを反復処理して検証し、特徴量重要度の辞書をインプレースで正規化する例:

python
request = {
    "method":  "POST",
    "path":    "/users",
    "headers": {"Content-Type": "application/json"},
    "body":    {"username": "はなこ", "email": "[email protected]"},
}

body   = request["body"]
errors = []

for field in ["username", "email"]:
    if not body.get(field):
        errors.append(f"Missing required field: {field}")

if "email" in body and "@" not in body["email"]:
    errors.append("Invalid email format")

print(f"Method: {request['method']} {request['path']}")
if errors:
    print(f"Errors: {errors}")
else:
    print("Validation passed")

# 特徴量重要度の値を合計1になるように正規化
feature_importance = {"age": 0.34, "income": 0.28, "region": 0.15, "purchases": 0.23}
total = sum(feature_importance.values())

for key in feature_importance:
    feature_importance[key] = round(feature_importance[key] / total, 3)

print(f"Normalised: {feature_importance}")