[Python] データ構造、どれ使う？

# Pythonでのデータ構造の保持方法について

Pythonにおいてデータを保持するクラスはいくつかありますが、その使い分けについて備忘録がてら記事を作成します。この記事では、Pythonの代表的なデータ構造である `tuple`, `dict`, `collections.namedtuple`, `types.SimpleNamespace`, `typing.NamedTuple`, `dataclasses.dataclass` の特徴と、それぞれの使い分けについてまとめています。

## 1. Pythonの主要なデータ構造とそれぞれの特徴について

### `tuple`
**不変なデータ構造の基本形**

タプルは、複数の要素を順序付けて格納できるイミュータブル（不変）なデータ型です。一度作成すると、その内容を変更することはできません。

* **特徴**:
    * イミュータブル（変更不可）
    * 要素へのアクセスはインデックス (`my_tuple[0]`)
    * 軽量で高速
    * 辞書のキーとして使用可能
* **定義例**:
    ```python
    point = (10, 20)
    colors = ('red', 'green', 'blue')
    ```
* **主な用途**:
    * 変更されたくない固定的な値の組（座標、RGB値など）
    * 関数の返り値として複数の値を返す場合
* **メリット**: シンプル、メモリ効率が良い、不変性が保証される
* **デメリット**: インデックスでのアクセスは、要素数が増えると可読性が低下する

### `dict`
**キーと値のマッピング**

辞書は、キーと値のペアを格納するミュータブル（可変）なデータ型です。キーを使って効率的に値にアクセスできます。

* **特徴**:
    * ミュータブル（変更可能）
    * キーを使って値にアクセス (`my_dict['key']`)
    * キーは一意でハッシュ可能である必要がある
    * Python 3.7以降では挿入順序が保持される[^1]
* **定義例**:
    ```python
    person = {'name': 'Alice', 'age': 30}
    config = dict(host='localhost', port=8080)
    ```
* **主な用途**:
    * JSONのような構造化データの表現
    * 設定情報やパラメータの管理
    * オブジェクトの属性を動的に格納
* **メリット**: 柔軟性が高い、キーによる直感的なアクセス
* **デメリット**: タプルに比べてメモリ消費がやや大きい傾向、属性アクセス (`obj.key`) はできない

### `collections.namedtuple`
**名前でアクセスできるタプル**

`namedtuple` は、Python の `collections` モジュールに含まれるファクトリ関数で、タプルのサブクラスとして各要素に名前（フィールド名）を付けてアクセスできるようにしたものです。タプルの軽量さと不変性を保ちつつ、可読性を向上させます。

* **特徴**:
    * イミュータブル
    * インデックスと属性名の両方でアクセス可能 (`point[0]`, `point.x`)
    * タプルベースなのでメモリ効率が良い
    * `_asdict()`, `_replace()` などのヘルパーメソッドを持つ
* **定義例**:
    ```python
    from collections import namedtuple
    Point = namedtuple('Point', ['x', 'y'])
    p = Point(10, 20)
    ```
* **主な用途**:
    * CSVの行データやデータベースのレコードのような、構造が固定されたデータの表現
    * 関数の複数の返り値を分かりやすくまとめたい場合
* **メリット**: 可読性が向上、タプルの利点を継承、軽量、Pythonの標準モジュールに入っている
* **デメリット**: 型ヒントの直接的なサポートはない
* **追記**:
    * namedtupleのファクトリ関数は第二引数に`str`をとった場合、カンマをスペースに置換した上でスペースを区切り文字としてsplit()を呼び出す、すなわち上の例は
        ```python
        Point = namedtuple('Point', 'x y')
        # もしくは
        Point = namedtuple('Point', 'x,y')
        ```
        と書いてもよい
    * `rename=True` オプションを指定すると、適切でないフィールド名（予約語や名前の重複など）は自動的に位置を示す名前に置き換えられる[^2]
        例えば `Point = namedtuple('Point', ['abc', 'def', 'ghi', 'abc'], rename=True)` とした場合、フィールド名は `('abc', '_1', 'ghi', '_3')` になる
        `rename=False` (デフォルト) の場合は `ValueError` が発生する
    * 型を付けたい場合は後述の`typing.NamedTuple`を使うとよい
    * Python3.7以降ではデフォルト値が設定できるようになった
        ```python
        Point = namedtuple('Point', ['x', 'y', 'z'], defaults=[0, 1])
        ```
        と書くことで`y`のデフォルト値が`0`に、`z`のデフォルト値が`1`に設定される

### `types.SimpleNamespace`
**手軽な属性アクセスオブジェクト**

`SimpleNamespace` は `types` モジュールにあるシンプルなオブジェクトで、属性の追加・アクセスがドット表記で可能です。インスタンス化時にキーワード引数で属性を初期化できます。

* **特徴**:
    * ミュータブル
    * 属性名でアクセス (`config.host`)
    * インスタンス化後も属性の追加・変更・削除が可能
    * `__repr__` など基本的な特殊メソッドは提供される
* **定義例**:
    ```python
    from types import SimpleNamespace
    config = SimpleNamespace(host='localhost', port=8080, debug=True)
    print(config.host)  # 出力: localhost
    config.user = 'admin' # 後から属性を追加可能
    print(config.user)  # 出力: admin
    ```
* **主な用途**:
    * 少数の属性をまとめて手軽に扱いたい場合
    * `dict` のキーアクセスよりも属性アクセスを好む場合
    * 設定値のグループ化や、一時的なデータの入れ物として
* **メリット**: 非常に手軽、属性アクセスが直感的
* **デメリット**: 型ヒントの直接的なサポートはない、不変性がない、`namedtuple`や`dataclass`ほどの機能はない

### `typing.NamedTuple`
**型ヒント付きのnamedtuple**

`typing.NamedTuple` は `collections.namedtuple` と似ていますが、クラス構文を使い、各フィールドに型ヒントを付与できる点が大きな特徴です。

* **特徴**:
    * イミュータブル
    * インデックスと属性名の両方でアクセス可能
    * フィールドごとに型ヒントを明記
    * デフォルト値を指定可能 (Python 3.6.1以降)[^3]
    * クラス構文で定義
* **定義例**:
    ```python
    from typing import NamedTuple

    class Point(NamedTuple):
        x: int
        y: int = 0 # デフォルト値を指定
    
    p1 = Point(10) # yはデフォルト値の0になる
    p2 = Point(x=5, y=15)
    ```
* **主な用途**:
    * 型安全性を重視する場面でのnamedtuple
    * APIのデータ構造定義
    * `collections.namedtuple` の型ヒント強化版として
* **メリット**: 型安全、可読性が高い、デフォルト値サポート、静的解析ツールとの親和性
* **デメリット**: `collections.namedtuple` に比べてやや記述量が増える

### `dataclasses.dataclass`
**メソッド付きデータ構造**

`dataclass` デコレータは、主にデータを保持するためのクラスを簡単に作成する機能を提供します。`__init__`, `__repr__`, `__eq__`, `__hash__` (条件による) などの特殊メソッドを自動生成してくれるため、ボイラープレートコードを大幅に削減できます。

* **特徴**:
    * デフォルトでミュータブル（`frozen=True` でイミュータブルに変更可能）
    * 属性名でアクセス
    * フィールドごとに型ヒントを推奨（必須ではないが一般的）
    * デフォルト値、初期化後の処理 (`__post_init__`)、フィールドごとのメタデータなど高機能
    * 比較メソッドなどを自動生成
    * 継承も可能
* **定義例**:
    ```python
    from dataclasses import dataclass, field

    @dataclass(frozen=True) # イミュータブルにする場合
    class InventoryItem:
        name: str
        unit_price: float
        quantity_on_hand: int = 0

        def total_cost(self) -> float:
            return self.unit_price * self.quantity_on_hand
    
    item = InventoryItem("Apple", 0.5, 100)
    print(item)
    print(item.total_cost())
    ```
* **主な用途**:
    * 状態を持つオブジェクトの定義
    * APIのレスポンス/リクエストモデル
    * 設定オブジェクト
    * メソッドを持つ簡単なクラスを素早く作りたい場合
* **メリット**: ボイラープレートコードを大幅削減、高機能で柔軟、型ヒントとの親和性が高い、可変/不変を選択可能
* **デメリット**: `namedtuple` 系に比べると、ややオーバーヘッドが大きい場合がある(`slots=True` (Python 3.10+)で改善可)[^4]

### 通常の `class`
**すべてを制御できる基本形**

属性やメソッドを自由に定義でき、最も柔軟性が高い方法です。

* **特徴**:
    * ミュータビリティは設計次第で可変にも不変にもできる
    * 属性アクセス (`obj.field`)
    * メソッドを自由に定義可能
    * 継承、カプセル化、ポリモーフィズムなど、オブジェクト指向の全機能を活用可能
    * `__init__`, `__repr__`, `__eq__` などの特殊メソッドは手動で実装
* **定義例**:
    ```python
    class Point:
        def __init__(self, x: int, y: int):
            self.x = x
            self.y = y

        def __repr__(self) -> str:
            return f"Point(x={self.x}, y={self.y})"

        def distance_from_origin(self) -> float:
            return (self.x**2 + self.y**2)**0.5
    ```
* **主な用途**:
    * データだけでなく、複雑な振る舞い（メソッド）を持つオブジェクトを定義する場合
    * `dataclass` の自動生成機能では不足する場合や、より細かな制御が必要な場合
    * 既存のライブラリやフレームワークのクラスを継承する場合
* **メリット**: 最大限の自由度と制御
* **デメリット**: 単純なデータコンテナとしては記述量が多くなる（`dataclass` はこの点を解消）

---

## 2. データ構造の比較と使い分け

ここからは、紹介したデータ構造を比較し、どのような場合にどれを選択すべきかの指針を示します。

### 特徴比較表 

| 特徴 | `tuple` | `collections.namedtuple` | `dict` | `types.SimpleNamespace` | `typing.NamedTuple` | `dataclasses.dataclass` | 通常の`class` |
| :--- | :------ | :----------------------- | :----- | :---------------------- | :------------------ | :---------------------- | :------------ |
| **不変性** | 不変 | 不変 | 可変 | 可変 | 不変 | 可変 (or 不変) | 設計次第 |
| **アクセス** | インデックス | インデックス, 属性 | キー | 属性 | インデックス, 属性 | 属性 | 属性 |
| **フィールド名** | なし | あり | あり (キー) | あり | あり | あり | あり |
| **型ヒント** | 間接的 | なし (直接) | 間接的 | なし (直接) | ◎ (必須) | ◎ (推奨) | 手動 |
| **デフォルト値** | なし | あり (Python 3.7+)[^2] | `get()`等 | なし | あり | あり | 手動 |
| **自動メソッド** | なし | `_asdict()`等 | なし | `__repr__`等 | `__init__`, `__repr__`等 | `__init__`等 | 手動 |
| **手軽さ** | ◎ | ◯ | ◎ | ◎ | △ | △ | × |
| **主な用途** | いくつかの値をセットで扱い、その順番や中身が変わらないことを保証したい場面 | 決まった構造のデータを読みやすく扱いたいが、中身は変えたくない場面 | 後から値を追加したい、また名前を使って簡単に情報を取り出したい場面 | いくつかの属性を手軽にまとめて使いたい場面 | `namedtuple`の便利さに加え、型安全にしたい場面 | 主にデータを保持するためのクラスを少ない手間で定義し、すぐに使い始めたい場面 | データとメソッドを組み合わせて、より複雑なものを作りたい場面 |

---

### どれ使う？

* **単純な不変シーケンスで、フィールド名が不要な場合**: `tuple`
    * 例: 関数の引数として固定的な少数の値を渡す、座標 `(x, y)` を一時的に保持するが名前アクセスは不要
* **不変で、フィールド名によるアクセスで可読性を上げたい、かつ手軽に定義したい場合 (型ヒントが最優先でない場合)**: `collections.namedtuple`
    * 例: CSVの行データ、DBのレコード、関数の複数の返り値をまとめる
* **可変で、キーと値のペアを柔軟に扱いたい場合、JSONライクなデータ**: `dict`
    * 例: 設定情報、動的にキーが増減するデータ、JSONデータのパース結果
* **ミュータブルで属性アクセスしたい、超手軽なオブジェクトが必要な場合**: `types.SimpleNamespace`
    * 例: ちょっとした設定のグループ化、名前空間として一時的に値をまとめたいとき
* **不変で、フィールド名と型ヒントが明確に必要な場合、特に静的解析の恩恵を受けたい場合**: `typing.NamedTuple`
    * 例: `collections.namedtuple` の型ヒント強化版。APIのデータ構造定義など
* **型ヒントが必須で、デフォルト値や比較メソッドなどの自動生成が欲しい、可変性も制御したい、よりオブジェクト指向的なデータ構造**: `dataclasses.dataclass`
    * 例: 状態を持つオブジェクト、APIのレスポンス/リクエストモデル、設定オブジェクト。`typing.NamedTuple` より高機能
* **データだけでなく複雑なロジックも持たせたい、最大限の自由度が必要な場合**: 通常の `class`
    * 例: 独自のメソッドや複雑な継承関係を持つオブジェクト

**選択のフローチャート:**

データをどのように扱いたいか？
│
├── 単純な値の組で、不変、順序が重要、名前アクセス不要 → tuple
│
├── 不変で、名前アクセスが欲しい
│   │
│   ├── 型ヒントが必須、デフォルト値も欲しい → typing.NamedTuple
│   │                                       (より高機能なら dataclass(frozen=True))
│   └── 手軽さ重視、型ヒントは厳密でなくてよい → collections.namedtuple
│
├── 可変で、キーと値のペア、柔軟性が重要 → dict
│
├── 可変で、属性アクセスが欲しい、超手軽に → types.SimpleNamespace
│
└── 型ヒントを活用し、メソッド自動生成、可変/不変を選択したい、よりオブジェクト指向的 → dataclasses.dataclass
│
└── さらに複雑なロジックや完全な制御が必要 → 通常の class

[^1]: https://docs.python.org/ja/3.7/reference/datamodel.html#the-standard-type-hierarchy
[^2]: https://docs.python.org/ja/3/library/collections.html#collections.namedtuple
[^3]: https://docs.python.org/ja/3.6/library/typing.html
[^4]: https://www.python.jp/news/wnpython310/dataclass-with-slots.html