月影

日々の雑感

【E資格対策】Pythonデータ構造の計算量とメモリ効率を徹底解説 その1

 

E資格対策:Pythonデータ構造の「なぜ?」を解く

リスト、辞書、集合の計算量とメモリ効率を徹底解説

深層学習のコーディングでは、大量のデータを高速に処理するために、Pythonの基本データ構造(リスト、タプル、辞書、集合)の「内部の仕組み」と「効率(計算量)」を理解することが不可欠です。

この記事では、E資格の試験でも頻出する、ミュータブル/イミュータブルハッシュO(1) などの重要概念を基礎から解説します。

0. 基礎知識:Pythonのデータ構造と重要用語の定義

0-1. Pythonの主要なデータ構造の定義

まずは、Pythonでデータを扱う4つの基本的な「箱」の特性を整理しましょう。

データ構造 表現方法 順序 変更 (Mutable) 特徴・用途
リスト (List) [1, 'a'] あり 可 (Mutable) データの集合を柔軟に変更・管理したい場合。
タプル (Tuple) (1, 'a') あり 不可 (Immutable) データの組を固定して安全に扱いたい場合。
辞書 (Dict) {'k': 1} あり
(ver3.7以降)
可 (Mutable) キーと値のペアで高速にデータを検索したい場合。キーは不変である必要があります。
集合 (Set) {1, 2} なし 可 (Mutable) 重複を許さず、高速な所属確認や集合演算をしたい場合。

0-2. 🔑 根幹をなす重要概念

A. ミュータブル(Mutable: 可変)とイミュータブル(Immutable: 不変)

この2つの違いは、辞書のキーになれるかどうかの決定的な差になります。

  • ミュータブル (リスト, 辞書, 集合): 作成後に中身の変更(追加、削除、代入)が可能。→ 辞書のキーや集合の要素になれない。
  • イミュータブル (タプル, 文字列, 数値): 作成後に中身を変更できない。→ 辞書のキーや集合の要素になれる。

B. ハッシュ(Hash)とハッシュ可能(Hashable)

💡 ハッシュ(Hash)とは?

任意の長さの入力データから、ある規則に従って生成される固定長の数値(ハッシュ値のことです。データの「指紋」のようなものと考えてください。

  • ハッシュ可能 (Hashable): オブジェクトの生存期間中、常に同じハッシュ値を返すことができる性質。
  • なぜ必要か: 辞書と集合は、このハッシュ値を計算してデータの格納場所を一瞬で特定(O(1))します。中身が変わってしまうミュータブルなオブジェクトは、格納後にハッシュ値が変わるとデータを見失うため、「ハッシュ可能」なイミュータブルなオブジェクトしかキーや要素になれません。

1. 🔑 不変性(Immutable)とハッシュ(Hash)の絶対法則

Pythonのデータ構造には、「中身を変えられるか否か」で大きく二つの性質があります。この違いが、辞書や集合の効率を支える鍵となります。

Q1/Q17の背景:辞書・集合のキーの制約

辞書 (dict) のキーや集合 (set) の要素は、検索を高速化するためにハッシュ可能(Hashable)でなければなりません。

  • タプル (1, 2) はイミュータブルなのでハッシュ可能 → キーに使える。
  • リスト [1, 2] はミュータブルなのでハッシュ不可能 → キーに使えない。

もしリストをキーにできてしまうと、リストの中身が変わったときにハッシュ値が変わり、データがどこに行ったか分からなくなるため、Pythonはこれをエラー(TypeError: unhashable type: 'list')とします。

2. ⚡️ 辞書と集合の驚異的な効率:O(1) の秘密

E資格では、データ構造ごとの操作の効率(計算量、Big-O)の理解が必須です。特に「検索速度」の違いは重要です。

2-1. 所属確認(in 演算子)の計算量(Q2)

データ構造 所属確認 (x in data) 計算量 理由(N が大きい場合)
リスト / タプル 遅い O(N)
(線形時間)
要素を最初から順番に全て探す必要がある(線形探索)。データ量に比例して時間がかかる。
辞書 / 集合 速い O(1)
(定数時間)
ハッシュテーブルにより、格納場所を一瞬で計算できる。データ量が増えても時間は変わらない。

辞書と集合の速度の秘密はハッシュテーブルにあります。データが100万件に増えても、検索ステップ数はほぼ変わらない(O(1))ため、機械学習のデータ前処理では頻繁に使用されます。

2-2. 集合の特性(順序の欠如)(Q4, Q8)

辞書や集合が O(1) の高速検索を実現する代償として、「順序」を犠牲にしています。

  • Q4(アクセス): 集合は順序を持たないため、リストのようにインデックス(添字)を使ったアクセス(s[0])はできません(TypeError)。
  • Q8(演算): 集合は数学的な集合演算(和集合、積集合、差集合)をサポートします。共通部分(積集合)を求めるには、ビット演算子& を使います。

3. 💾 リスト操作とメモリ管理の落とし穴

リスト操作やコピー操作には、メモリの参照に関する注意点があり、特にバグの原因になりやすいためE資格で重要視されます。

3-1. リストへの追加方法(Q3)

a = [1, 2]
b = [3, 4]

# append: 「1つの要素」として追加(ネストする)
a.append(b) 
# 結果: [1, 2, [3, 4]]

# extend: 中身を「展開」して追加
a = [1, 2]
a.extend(b)
# 結果: [1, 2, 3, 4]

3-2. 浅いコピー(Shallow Copy)の危険性(Q5)

list_b = list_a.copy() は浅いコピーです。これは「外箱」だけを新しく作り、中身は元のデータを共有する方式です。

list_a = [[1, 2], [3, 4]]
list_b = list_a.copy() # 浅いコピー

# list_b を変更したつもりが...
list_b[0][0] = 9 

# 実は list_a も変わってしまう(実体を共有しているため)
# list_a[0][0] も 9 になります。

完全に独立させるには、import copy を使った深いコピー(copy.deepcopy()が必要です。

3-3. 辞書の安全なアクセス(Q6)

キーが存在しない場合にエラー(KeyError)を回避しつつ値を取得する最も良い方法は、d.get(key, default_value) メソッドです。

  • 非推奨: d['b'] → キーがないとエラーになり、プログラムが止まる。
  • 推奨: d.get('b', 0) → キーがないと指定したデフォルト値 0 を返す。例外処理が不要になりコードが簡潔になる。

4. ⚙️ 特殊な構文と効率的な処理

4-1. ジェネレータ式とメモリ効率(Q9)

Python(i for i in range(10)) のように丸括弧を使った内包表記は、タプルではなくジェネレータ式を作成します。

ジェネレータのメリット

遅延評価: 値をメモリに一括で作成せず、必要な時に一つずつ生成します。
100万件などの大規模なデータを扱う際、リスト内包表記のように全要素をメモリに保持する必要がないため、メモリ効率が非常に高いです。

4-2. 拡張アンパッキング(Q7)

head, *mid, tail = data のような構文は、残りの要素(2, 3, 4)を自動的に mid に格納する拡張アンパッキングです。このとき、* が付いた変数には、必ずリストとして値が代入されるというルールがあります。

4-3. 辞書の順序の保証(Q10

Python 3.7以降: 辞書 (dict) は、要素が追加された挿入順序を保持することが正式な仕様として保証されました。

d['b'] = 2, d['a'] = 1 と追加すれば、ループ順序は常に ba となり、コードの再現性が向上しています。