閉じる

Pythonのin演算子は遅い? リストとセット/辞書の計算量比較

大量のデータから特定の値が含まれているかを調べるとき、Pythonのin演算子は本当に遅いのでしょうか。

結論から言うと、コレクションの種類によって大きく異なります。

リストはO(n)、セットや辞書は平均O(1)という違いがあるため、使い分けがとても重要です。

この記事では、仕組みと計算量、注意点、実践的な使い分けを丁寧に解説します。

Pythonのin演算子とは?初心者向けの基本

要素が含まれるかを調べる演算子

inは、指定した要素がコレクションに含まれているかを真偽値で返す演算子です。

否定形のnot inもあります。

いずれも読みやすく、Pythonでの存在チェックの第一候補になります。

Python
# 基本的なinの使い方

# リスト(list)
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits)   # True
print("grape" not in fruits)  # True

# セット(set)
colors = {"red", "green", "blue"}
print("green" in colors)    # True
print("yellow" in colors)   # False

# 辞書(dict) - 注意: inは「キー」に対して判定します
person = {"name": "Alice", "age": 20}
print("name" in person)     # True (キー"name"がある)
print("Alice" in person)    # False (値は直接は対象外)
実行結果
True
True
True
False
True
False

対象コレクションはリスト・セット・辞書

Pythonでよく使うコレクションのうち、存在チェックで主役になるのはリスト(list)、セット(set)、辞書(dict)です。

それぞれの判定対象の違い計算量の目安を整理しておきます。

コレクションinが判定する対象重複順序保持計算量の目安
list要素の値可(挿入順)O(n)
set要素の値不可不定(論理的に順序なし)平均O(1)、最悪O(n)
dictキーキーは一意可(挿入順)平均O(1)、最悪O(n)

辞書で値やペアを調べたい場合は、values()items()を使います。

Python
# dictで値や(キー, 値)の存在を調べる
person = {"name": "Alice", "age": 20}

print("Alice" in person.values())   # 値に"Alice"があるか → True
print(("age", 20) in person.items())  # ("age", 20)というペアがあるか → True
実行結果
True
True

計算量の違い:リストはO(n)、セット/辞書は平均O(1)

リストの探索は先頭から順に確認する(O(n))

リストのinは、先頭から順に値を比較し、見つかったところで止まります。

要素がなければ最後まで全走査となり、データ量に比例して時間が伸びるためO(n)です。

次のコードで、リストがどれだけ比較を行うかを目で確認できます。

Python
# リストのinが先頭から順に比較していることを可視化するサンプル

class Counter:
    comparisons = 0  # 比較回数を数えるためのクラス変数

class Probe:
    """何と比較してもFalseを返すオブジェクト。比較回数だけ数える。"""
    def __eq__(self, other):
        Counter.comparisons += 1
        return False

# ケース1: どれとも一致しない(最後まで探索する)
lst = [Probe() for _ in range(5)]
print(object() in lst)  # 常にFalse
print("比較回数:", Counter.comparisons)  # 5回に等しい

# ケース2: 途中で一致したことにするオブジェクト
class MatchFirst:
    def __eq__(self, other):
        Counter.comparisons += 1
        return True  # 最初の比較で一致したとみなす

Counter.comparisons = 0
lst2 = [MatchFirst(), Probe(), Probe()]
print(object() in lst2)  # 最初でTrueになり探索が止まる
print("比較回数:", Counter.comparisons)  # 1回に等しい
実行結果
False
比較回数: 5
True
比較回数: 1

このように、リストでは見つかる位置や見つからない場合で比較回数が大きく変わることが分かります。

セット/辞書はハッシュ表で高速に判定する(平均O(1))

セットと辞書は内部的にハッシュ表を用います。

要素やキーからハッシュ値を計算し、対応する「バケット」に直接アクセスするため、平均的にはデータ量に依存しない平均O(1)で判定できます。

これが、存在チェックでset/dictが圧倒的に速い理由です。

Python
# リストとセット/辞書でinの速度をざっくり比べる簡易ベンチマーク
# 実行環境で差は出ますが、増加傾向の違いを感じられます。

import time

def bench(n=100_000, trials=10_000):
    data = list(range(n))
    s = set(data)
    d = {x: None for x in data}

    hit = n - 1   # 末尾(リストの「見つかるが遅い」例)
    miss = -1     # 存在しない(リストの最悪側)

    def run(label, container, needle):
        start = time.perf_counter()
        for _ in range(trials):
            needle in container
        end = time.perf_counter()
        return f"{label}: {(end - start)*1e3/trials:.6f} ms/回"

    print(f"N={n}, trials={trials}")
    print(run("list hit ", data, hit))
    print(run("list miss", data, miss))
    print(run("set  hit ", s, hit))
    print(run("set  miss", s, miss))
    print(run("dict hit ", d, hit))
    print(run("dict miss", d, miss))

bench(100_000, trials=5_000)
実行結果
N=100000, trials=5000
list hit : 0.347539 ms/回
list miss: 0.349541 ms/回
set  hit : 0.000035 ms/回
set  miss: 0.000013 ms/回
dict hit : 0.000018 ms/回
dict miss: 0.000014 ms/回

このように、データ件数Nを増やすと、listは1回あたりの時間が伸びやすい一方で、set/dictはほぼ一定という傾向が確認できます。

なおかつlistよりも非常に高速です。

辞書のinは「キーのみ」を判定する点に注意

辞書のinは、キーの存在だけを判定します。

値に対する判定はvalue in d.values()、(キー, 値)の組に対する判定は(k, v) in d.items()を使います。

値が含まれるかをvalue in dで調べてもFalseになるので注意してください。

Python
d = {"id": 123, "name": "Alice"}
print("name" in d)             # True: キーの存在
print("Alice" in d)            # False: 値は対象外
print("Alice" in d.values())   # True: 値の存在チェック
実行結果
True
False
True

最悪ケースはO(n)もあるが、まずは平均的な速さを覚えよう

ハッシュ表は衝突(同じバケットに複数の要素が集まること)が多発すると、内部的に再探索が増えて最悪O(n)に近づく可能性があります。

しかし一般的なデータと実装では、平均O(1)の高速性が得られると考えて問題ありません。

初心者のうちは、存在チェックはset/dictが速いと覚えるのが実践的です。

使い分けガイド:リスト vs セット/辞書の選び方

存在チェックが主目的ならsetが速い

大量データに対し、ある値が含まれているかだけを何度も調べるならsetを選ぶのが定石です。

リストから一度セットを作れば、その後のinは平均O(1)で判定できます。

Python
# 大量データでの存在チェックはsetに変換してから
users = [f"user{i}" for i in range(200_000)]

# 検索を繰り返すなら、最初にsetへ変換しておく
user_set = set(users)

targets = ["user199999", "userX", "user1000"]
for t in targets:
    print(t, t in user_set)  # 平均O(1)で高速
実行結果
user199999 True
userX False
user1000 True

キーで値を引くならdict一択

キーが分かっていて対応する値が欲しいならdictが最適です。

inでキーの存在チェックを行い、続けてd[key]で取得する流れはシンプルかつ高速です。

Python
# IDからユーザー名を引く
name_by_id = {100: "Alice", 200: "Bob", 300: "Carol"}

uid = 200
if uid in name_by_id:          # キーの存在チェック
    print(name_by_id[uid])     # 値を取り出す(平均O(1))
実行結果
Bob

順序を保ちたい・重複OKならlist

入力順のまま扱いたい、同じ値が複数あってもよい、合計件数がそれほど多くない、といった要件ではlistが扱いやすいです。

inの速度よりも、順序や重複、整列のしやすさを優先できます。

データが小さいなら可読性優先でOK

数十〜数百件程度までなら、inの実行時間は体感的に問題にならないことが多いです。

読みやすさを優先し、必要になってからset/dict化を検討しても遅くありません。

大量データ(1万件以上)はset/dictでパフォーマンス改善

データが1万件を超え、しかも存在チェックの回数が多いなら、最初にset/dictへ変換するだけでオーダー改善による大幅な高速化が期待できます。

変換コストは一度きりなので、繰り返し検索するワークロードではすぐに回収できます。

注意点とベストプラクティス

set/dictの要素・キーはハッシュ可能(immutable)な型を使う

セットや辞書のキーにはハッシュ可能(hashable)な型が必要です。

代表的なOK例はintstrtuple(中身もimmutable)frozensetなどです。

listやdictなどmutableな型は不可です。

Python
# ハッシュ不可の例: listはsetに入れられない
try:
    s = set()
    s.add([1, 2, 3])  # TypeError
except TypeError as e:
    print("エラー:", e)

# ハッシュ可能な代替: tupleやfrozensetを使う
s = set()
s.add((1, 2, 3))            # OK: tupleはimmutable
s.add(frozenset({1, 2, 3})) # OK: frozensetはimmutable
print("要素数:", len(s))
実行結果
エラー: unhashable type: 'list'
要素数: 2

ハッシュ計算にもコストがある(複雑なオブジェクトは注意)

set/dictの平均O(1)は、hash(x)の計算と少数回の等値比較が前提です。

オブジェクトが大きい、__hash__/__eq__が高コスト、文字列が非常に長いなどの場合、1回の判定が相対的に重くなることがあります。

とはいえ、リストに比べれば多くの場面でset/dictが有利です。

大量に同じ文字列で判定するなら、Pythonの文字列は一度計算したハッシュを内部的にキャッシュするため、繰り返し判定での負担は小さくなります。

文字列は大文字小文字や正規化で一致条件をそろえる

文字列の存在チェックは、表記ゆれで取り逃しが起きがちです。

大小文字の違いやUnicodeの正規化(NFC/NFKCなど)をそろえてからset/dictを作ると堅牢です。

Python
# 文字列の存在チェックを、大小文字とUnicode正規化をそろえて行う
import unicodedata

raw = ["Cafe", "café", "CAFE"]
# 正規化+casefoldで基準化してからset化
normalized = {unicodedata.normalize("NFC", s).casefold() for s in raw}

query = "CAFÉ"  # 表記が異なるが同じ概念
key = unicodedata.normalize("NFC", query).casefold()
print(key in normalized)  # True
実行結果
True

set/dictはメモリ使用量が増えやすい

ハッシュ表は高速化の代償としてメモリを余分に確保します。

要素数が多い場合は、メモリの上限やプロセスの制約にも配慮してください。

目安として、同じ内容でもset/dictはlistより大きなオーバーヘッドを持ちます。

Python
# ざっくりしたメモリ比較(コンテナ本体のみ)
# 実際の総メモリは要素分も加わるため、あくまで目安です。
import sys

n = 10_000
lst = list(range(n))
st = set(lst)
dc = {x: None for x in lst}

print("list容器サイズ:", sys.getsizeof(lst), "bytes")
print("set 容器サイズ:", sys.getsizeof(st), "bytes")
print("dict容器サイズ:", sys.getsizeof(dc), "bytes")
実行結果
list容器サイズ: 80056 bytes
set 容器サイズ: 524312 bytes
dict容器サイズ: 983312 bytes

listよりset/dictが大きい傾向が見て取れます。

実運用では速度とメモリのトレードオフを意識しましょう。

まとめ

存在チェックの速度は、コレクションの選択で劇的に変わります

リストは順次探索のためO(n)、セット/辞書はハッシュ表により平均O(1)で高速に判定できます。

辞書のinはキーに対する判定である点に注意し、表記ゆれやハッシュ可能性、メモリ使用量といった実務上のポイントも押さえましょう。

データが小さいうちは可読性を優先してもよいですが、1万件以上のデータで存在チェックを繰り返すならset/dictへの切り替えが有効です。

最終的には、要件(順序・重複・検索頻度)と制約(メモリ)を踏まえ、正しいコレクションを選ぶことが、Pythonコードのパフォーマンスと可読性を両立させる鍵になります。

Python 実践TIPS - コーディング効率化・Pythonic
この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!