【Python】レコードをIDで素早く探す:辞書の索引と重複IDの検出

PythonのTopに戻る


辞書のリストから同じIDを何度も探すなら、先にIDをキーとする辞書の索引を作ると、毎回全件を走査せずに取り出せる。ただし、辞書内包表記でいきなり変換すると、重複IDが後のレコードで上書きされる。索引を作る段階で重複を検出し、「存在しないID」とは別の問題として扱おう。

検索用の索引を一度作る

数件のデータから一度だけ探すなら、for文で順に比較しても十分である。一方、同じ一覧を使って多くのIDを繰り返し調べる場合は、最初に索引を用意する価値がある。辞書の検索は通常、全レコードを一件ずつ比較する処理とは異なる方法でキーを探せる。

索引にも作成時間と保存用のメモリが必要なので、どんな場合でも無条件に有利とは限らない。データの更新頻度や検索回数を考えて選ぶ。この記事では性能の測定値を示すのではなく、重複を見落とさない索引の作り方を確認する。

IDの重複を検出してから登録する

example.py

def build_index(records):
    index = {}
    for record in records:
        key = record["id"]
        if not isinstance(key, str) or not key:
            raise ValueError("id must be a nonempty string")
        if key in index:
            raise ValueError(f"duplicate id: {key}")
        index[key] = record
    return index


records = [{"id": "A", "value": 10}, {"id": "B", "value": 20}]
index = build_index(records)
print(index["B"])
print("missing:", index.get("Z"))
assert index["A"] is records[0]
assert index.get("Z") is None
try:
    build_index(records + [{"id": "A", "value": 99}])
except ValueError as exc:
    print(exc)
else:
    raise AssertionError("Duplicate ID was accepted")
for bad in ({"value": 1}, {"id": 1}, {"id": ""}):
    try:
        build_index([bad])
    except (KeyError, ValueError):
        pass
    else:
        raise AssertionError("Invalid ID was accepted")
assert build_index([]) == {}

実行結果

{'id': 'B', 'value': 20}
missing: None
duplicate id: A

key in indexを先に調べることで、同じIDが二回現れた場所で停止できる。レコードの内容がたまたま同じでも、この例では重複IDとして拒否する。一つのIDへ複数レコードを対応付けたい用途なら、リストへグループ分けする別の構造を選ぶ。

見つからない検索と不正な入力を分ける

index[“A”]は必ず存在する前提の取得であり、存在しなければKeyErrorになる。index.get(“Z”)はこの例ではNoneを返すため、見つからないことが普通に起こる検索へ使える。索引の値にNoneを保存する設計なら、別のセンチネルで区別する必要がある。

一方、入力レコードにidという項目がない場合もKeyErrorになり得るが、これは検索対象が見つからない状況とは違う。どの処理で起きたのかを見て、不完全な入力データとして扱う。必要ならレコード番号などを添えて、入力検査の段階で説明付きのエラーに変える。

IDの型と正規化ルールを決める

例では、空でない文字列だけをIDとして受け入れている。整数の1と文字列の”1″は別のキーだが、数値の1とTrueのように等しいと判定される組合せもある。IDの型を統一しておくと、意図しない一致や検索漏れを減らせる。

先頭の0、大文字と小文字、前後の空白を同じものとして扱うかどうかも、データの仕様次第である。何となくintへ変換したりstripやlowerを適用したりすると、別のIDを一つにまとめてしまう場合がある。正規化をするなら、その後のIDに重複がないかを改めて検査する。

索引と元レコードは同じ中身を参照する

この例のindexは、元のレコードへの参照を値として保存する。レコード全体を深くコピーしていないので、測定値を書き換えると索引経由でも変更後の値が見える。この性質を利用するか、元を保存するためにコピーするかを決めておく。

特にレコードのidを変更しても、索引のキーは自動で更新されない。古いキーが新しいIDのレコードを指す状態になり、検索の意味が崩れる。IDを不変として扱うか、変更時には古いキーを除き、新しいキーの重複を検査して登録し直す必要がある。

更新方法まで決めて使う

元の一覧にレコードを追加・削除しても、すでに作った索引は自動で同期されない。更新が少なければ、その都度作り直す方が単純である。頻繁に更新するなら、一覧と索引を同じ窓口から変更するようにして、片方だけを更新しないようにする。

確認では、通常検索、未知のID、重複ID、ID項目の欠落、不正なID型をそれぞれ試す。短い辞書変換で済ませず、入力の一意性と検索失敗を分けることで、一覧が大きくなっても結果を信頼しやすい索引になる。

動作確認と参考資料

掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。

関連するTips


PythonのTopに戻る