Pythonのコレクション型、多すぎ...???
新卒入社1年目の2月。私はとあるシステムの開発にメンバとして入っていました。このシステムは実行時間とメモリ使用量がギリギリになることが当初から想定されており、データをlistで持つかdictにするかという設計のトレードオフに (個人的に) 直面していました。
悩んだトレードオフ
ここでは、listとdict、その他の選択についての具体的なトレードオフの悩みを掘り下げていきます。
メモリと計算時間を確保するならlist
- メリット: listはPythonの中でも単純な構造でメモリ使用量を抑えられます。またインデックスアクセスはO(1)で可能です。
- デメリット: インデックスでアクセスする場合、後から見たときに可読性が低く、「なんのデータがどこに格納されてるんだっけ?」となりがちです。またリストの要素数が大きくなるとreallocateが時々発生します。とはいえ1要素ごとに起こるわけではなく、実測的にはそこまで大きな問題にはならないことが多いです。
可読性と柔軟性を確保するならdict
- メリット: dictはキーによるアクセスが可能なため、コードの可読性が大きく向上します。キーによるアクセスも通常O(1)で、要件次第ではlistとほぼ同等のパフォーマンスが期待できます。またPython 3.7以降では挿入順序を保持するため、順番が重要なデータでもある程度制御しやすくなりました。今回の実装の他の部分でdictの挿入順序が維持されることは使った記憶があります。
- デメリット: 内部的にハッシュテーブルを用いるため、要素数が同じでもlistよりもメモリ消費が高くなり2〜3倍以上かさむことも珍しくないとされているらしいですね (ChatGPT談)。
Array, NamedTuple, dataclassの可能性
-
Array
- メリット: メモリ効率が非常に高く、固定長&同じ型の数値データなどには最適。
- デメリット: 異なる型を混在させることはできません。今回のケースでは数字と文字列を混在させる必要があり不向きでした。
-
NamedTuple
- メリット: listと同様にメモリ効率が良く、名前付きフィールドで可読性が向上します。多分これが最善でした。
- デメリット: イミュータブル(変更不可)であり、特に現状のコードはミュータブルであることを使っていた記憶があるため、今からのリファクタは下手すると処理全体を書き直す必要があります。
-
dataclass
- メリット: 名前付きフィールドがあり柔軟に扱えます。可読性も保たれます。
- デメリット: NamedTupleや純粋なlistに比べてメモリ消費が大きくなりがちです。イミュータブルにしてメモリ消費量を抑える場合は
frozen=True
オプションを使う方法もあるらしいです (ChatGPT談)。
実際の選択とその理由
結局私はlistを選択しました。理由はメモリ制約と納期です。dictで実装してメモリが足りなかったり、メモリが足りなかったときのやり直しコストが高すぎると判断しました。
結果と教訓
- システム自体はちゃんと動いたので大きな失敗にはなりませんでしたが、可読性に関してはどうしても犠牲になってしまいました。後任者がコードを読む際に苦労するのを目の当たりにして「もう少しコレクション型について知識を持っておくべきだった、特にdictやNamedTupleを試してみるべきだったかも」と振り返っています。
大きな教訓としては以下の通りです。
- 優先順位を明確にする: メモリか速度か可読性か、それとも別の要素を重視するのか。
- トレードオフをドキュメント化する: なぜその選択肢を選んだか、後から説明できるようにまとめておく。
- 未来の拡張性を考える: 今は要らないかもしれないが、後々の仕様変更や運用を見越して設計する。
- 多様なコレクション型を学ぶ: listやdictだけでなく、NamedTupleやdataclassなどを知っておくと選択の幅が広がる。