Pythonでのデータ構造の保持方法について
Pythonにおいてデータを保持するクラスはいくつかありますが、その使い分けについて備忘録がてら記事を作成します。この記事では、Pythonの代表的なデータ構造である tuple
, dict
, collections.namedtuple
, types.SimpleNamespace
, typing.NamedTuple
, dataclasses.dataclass
の特徴と、それぞれの使い分けについてまとめています。
1. Pythonの主要なデータ構造とそれぞれの特徴について
tuple
不変なデータ構造の基本形
タプルは、複数の要素を順序付けて格納できるイミュータブル(不変)なデータ型です。一度作成すると、その内容を変更することはできません。
-
特徴:
- イミュータブル(変更不可)
- 要素へのアクセスはインデックス (
my_tuple[0]
) - 軽量で高速
- 辞書のキーとして使用可能
-
定義例:
point = (10, 20) colors = ('red', 'green', 'blue')
-
主な用途:
- 変更されたくない固定的な値の組(座標、RGB値など)
- 関数の返り値として複数の値を返す場合
- メリット: シンプル、メモリ効率が良い、不変性が保証される
- デメリット: インデックスでのアクセスは、要素数が増えると可読性が低下する
dict
キーと値のマッピング
辞書は、キーと値のペアを格納するミュータブル(可変)なデータ型です。キーを使って効率的に値にアクセスできます。
-
特徴:
- ミュータブル(変更可能)
- キーを使って値にアクセス (
my_dict['key']
) - キーは一意でハッシュ可能である必要がある
- Python 3.7以降では挿入順序が保持される1
-
定義例:
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()
などのヘルパーメソッドを持つ
-
定義例:
from collections import namedtuple Point = namedtuple('Point', ['x', 'y']) p = Point(10, 20)
-
主な用途:
- CSVの行データやデータベースのレコードのような、構造が固定されたデータの表現
- 関数の複数の返り値を分かりやすくまとめたい場合
- メリット: 可読性が向上、タプルの利点を継承、軽量、Pythonの標準モジュールに入っている
- デメリット: 型ヒントの直接的なサポートはない
-
追記:
- namedtupleのファクトリ関数は第二引数に
str
をとった場合、カンマをスペースに置換した上でスペースを区切り文字としてsplit()を呼び出す、すなわち上の例はと書いてもよい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以降ではデフォルト値が設定できるようになった
と書くことで
Point = namedtuple('Point', ['x', 'y', 'z'], defaults=[0, 1])
y
のデフォルト値が0
に、z
のデフォルト値が1
に設定される
- namedtupleのファクトリ関数は第二引数に
types.SimpleNamespace
手軽な属性アクセスオブジェクト
SimpleNamespace
は types
モジュールにあるシンプルなオブジェクトで、属性の追加・アクセスがドット表記で可能です。インスタンス化時にキーワード引数で属性を初期化できます。
-
特徴:
- ミュータブル
- 属性名でアクセス (
config.host
) - インスタンス化後も属性の追加・変更・削除が可能
-
__repr__
など基本的な特殊メソッドは提供される
-
定義例:
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
- クラス構文で定義
-
定義例:
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__
)、フィールドごとのメタデータなど高機能 - 比較メソッドなどを自動生成
- 継承も可能
- デフォルトでミュータブル(
-
定義例:
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__
などの特殊メソッドは手動で実装
-
定義例:
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
より高機能
- 例: 状態を持つオブジェクト、APIのレスポンス/リクエストモデル、設定オブジェクト。
-
データだけでなく複雑なロジックも持たせたい、最大限の自由度が必要な場合: 通常の
class
- 例: 独自のメソッドや複雑な継承関係を持つオブジェクト
選択のフローチャート:
データをどのように扱いたいか?
│
├── 単純な値の組で、不変、順序が重要、名前アクセス不要 → tuple
│
├── 不変で、名前アクセスが欲しい
│ │
│ ├── 型ヒントが必須、デフォルト値も欲しい → typing.NamedTuple
│ │ (より高機能なら dataclass(frozen=True))
│ └── 手軽さ重視、型ヒントは厳密でなくてよい → collections.namedtuple
│
├── 可変で、キーと値のペア、柔軟性が重要 → dict
│
├── 可変で、属性アクセスが欲しい、超手軽に → types.SimpleNamespace
│
└── 型ヒントを活用し、メソッド自動生成、可変/不変を選択したい、よりオブジェクト指向的 → dataclasses.dataclass
│
└── さらに複雑なロジックや完全な制御が必要 → 通常の class