標準ライブラリ拡張の設計レビュー:subclass vs wrapperの判断軸
はじめに
Pythonの標準ライブラリは、豊富な型とクラスで支えられている。
dict, list, datetime, Path, Exceptionなど、日常的に使われるこれらのクラスに独自ロジックを加えたくなる場面は少なくない。
その際、開発者は2つの選択肢に直面する:
- サブクラス化(継承)して拡張する
- 内部に保持(wrap)して機能を委譲する
レビューアーの役割は、この選択が妥当かどうかを構造的に評価し、将来的なメンテナンス性・バグ回避の観点から指摘することである。
継承とラップの基本的な違い
まず、構造上の違いを明確にしておく。
| 方法 | 特徴 |
|---|---|
| 継承 | 元クラスのインタフェース・動作をそのまま引き継ぎ、一部をオーバーライド |
| ラップ | 元クラスのインスタンスを保持し、必要なメソッドだけを委譲・追加 |
継承例:dictを拡張
class CaseInsensitiveDict(dict):
def __getitem__(self, key):
return super().__getitem__(key.lower())ラップ例:dictを保持
class CaseInsensitiveDict:
def __init__(self, data):
self._data = dict((k.lower(), v) for k, v in data.items())
def get(self, key, default=None):
return self._data.get(key.lower(), default)継承は便利で短く書けるが、元クラスのすべての動作を引き継ぐという重責を背負う。
一方、ラップは安全だが手間がかかり、インターフェース互換が部分的になるというトレードオフがある。
レビュー観点1:「継承が妥当な構造」かを検証する
継承には以下のようなリスクがある:
- 元クラスの仕様変更に引きずられる
- 内部実装に依存してしまい壊れやすくなる
- 特殊メソッド(
__contains__,__iter__)などの挙動が意図せず変化する
dictの継承での副作用例
class MyDict(dict):
def keys(self):
return ["fake_key"]Comment
@Reviewer: `keys()` のオーバーライドにより、`in`演算子や `for`ループの挙動が予期せず変わる可能性があります。dictの契約を破っていないか再検討してください。レビュー視点:
- そのクラスは「本当に」元クラスの一種(is-a関係)か?
- 全インターフェースの互換性が担保されるか?
__init__,__eq__,__hash__など特殊メソッドの動作に副作用はないか?
レビュー観点2:ラップによる過度な再発明を避ける
ラップは柔軟性があるが、標準的な操作(+演算子、len、比較演算など)を1つずつ手作業で再実装する羽目になることがある。
listのラップでの冗長化例
class MyList:
def __init__(self, items):
self._items = list(items)
def append(self, item):
self._items.append(item)
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]Comment
@Reviewer: listの機能をすべて再実装しており、保守コストが高くなっています。必要な拡張が1〜2個なら継承も検討に値します。レビュー視点:
- 拡張したい内容は「一部の操作」だけか、それとも構造全体を変えたいのか?
- 標準操作のうちどれを再実装する必要があるのかを見極めたか?
レビュー観点3:__init__と状態管理の独自実装に注意
標準クラス(特にdatetime, Exception, Path)を継承して初期化処理を追加する設計は、意図せず元クラスの構築ロジックを破壊する可能性がある。
Exceptionサブクラスでの初期化失敗
class CustomError(Exception):
def __init__(self, code):
self.code = codeComment
@Reviewer: `Exception` の初期化処理を継承しておらず、スタックトレースやメッセージが失われます。`super().__init__()` を必ず呼び出すようにしてください。レビュー視点:
- コンストラクタのパラメータは親と整合しているか?
- 初期化処理を追加した際、元クラスの機能が失われていないか?
どちらを採用すべきか?判断のための対照表
| 検討項目 | 継承(Subclass) | ラップ(Wrapper) |
|---|---|---|
| インターフェース互換性 | 完全互換(原則) | 部分互換(設計次第) |
| 保守性(親の変更の影響) | 影響を受けやすい | 独立している |
| 実装コスト | 低い | 高い |
| 安全性(副作用の少なさ) | 低い(壊れるリスク) | 高い(構造的制御可) |
| 「〜は〜である」関係があるか | Yes のときに適している | No のときに適している |
レビューコメントテンプレート例
継承を避けるべきパターン
@Reviewer: このクラスは `dict` の一種とは言えず、`isinstance`判定に誤解を招く可能性があります。ラップ構造への変更を検討してみてください。ラップが過剰な場合の指摘
@Reviewer: 単一メソッドの挙動のみ変更したい場合、継承+オーバーライドの方が保守性と共通性を保てる可能性があります。調整された設計:collections.UserDictやPathLikeの活用
Pythonには、サブクラス化における落とし穴を避けるために継承専用のベースクラスが提供されている場合がある。
collections.UserDict,UserList,UserStringos.PathLikeabc.ABC,collections.abc.Mapping等
これらを継承することで、元クラスの実装に依存せずに拡張だけを記述できる安全な構造が得られる。
UserDictを使った例
from collections import UserDict
class NormalizedKeyDict(UserDict):
def __getitem__(self, key):
return super().__getitem__(key.lower())Comment
@Reviewer: 標準dictの実装詳細に依存せず、`UserDict` を利用してインターフェースのみ継承している点は好ましい設計です。おわりに
標準ライブラリの拡張は、目の前の機能追加だけでなく、将来的なコードの壊れやすさを伴う設計判断である。
継承は簡便さの代償として壊れやすさを、ラップは安全性の代償として実装コストを求める。
レビューアーは「今できるか」ではなく、「5年後も安心して使えるか」という視点で、継承とラップの構造判断を支援すべきである。