Python | OOP:getitem

Python Python
スポンサーリンク

概要(getitemは「obj[i]やスライス」を使えるようにする特別メソッド)

getitemは、obj[i] や obj[a:b] のようなインデックス・スライスアクセスを可能にする入口です。シーケンスっぽいクラス(list風・文字列風・配列風)を自作するときに不可欠で、「何を返すか」「範囲外をどう扱うか」「スライスはどう返すか」を決めるのが核心です。インデックスは通常0始まり、範囲外はIndexErrorを投げる、スライスはsliceオブジェクトを受け取り適切に処理する、というのが基本ルールです。


基本の使い方(インデックスと範囲外の扱い)

最小の実装と動作

class Cart:
    def __init__(self):
        self._items: list[tuple[str, int]] = []  # (SKU, 数量)

    def add(self, sku: str, qty: int):
        self._items.append((sku, qty))

    def __getitem__(self, index: int) -> tuple[str, int]:
        # listに委譲するだけでもOK(IndexErrorはlistが投げる)
        return self._items[index]

c = Cart()
c.add("A001", 2)
c.add("B002", 1)
print(c[0])  # ('A001', 2)
print(c[1])  # ('B002', 1)
# print(c[2])  # IndexError(範囲外)
Python

「範囲外はIndexError」「型が不正ならTypeError」を守ると、Python標準の振る舞いと揃い、ユーザーが迷いません。

型が不正なキーへの対応(TypeError)

class Cart:
    def __init__(self):
        self._items = []
    def add(self, sku, qty):
        self._items.append((sku, qty))
    def __getitem__(self, index):
        if not isinstance(index, int):
            raise TypeError("indexはint")
        return self._items[index]
Python

数値以外の「キー」を受け付けない設計なら、TypeErrorで早めに弾きます。


スライスの扱い(sliceオブジェクトを受けて部分列を返す)

スライスに対応する基本パターン

class Cart:
    def __init__(self):
        self._items: list[tuple[str, int]] = []
    def add(self, sku: str, qty: int):
        self._items.append((sku, qty))

    def __getitem__(self, key):
        if isinstance(key, int):
            return self._items[key]  # 負のインデックスもlistが処理
        if isinstance(key, slice):
            # スライスは同じ型で返すか、listで返すかを決める
            new = Cart()
            for sku, qty in self._items[key]:
                new.add(sku, qty)
            return new
        raise TypeError("keyはintまたはsliceに対応")

c = Cart()
for i in range(5):
    c.add(f"SKU{i}", i+1)
sub = c[1:4]          # Cartを返す(1〜3番目)
print(sub[0])         # ('SKU1', 2)
Python

スライスを「同じ型で返す」か「listで返す」かは設計次第です。自作シーケンスなら「同じ型で返す」と使い心地が良くなります。

slice.indicesで正規化してから処理する

class Buffer:
    def __init__(self, text: str = ""):
        self._text = text

    def __getitem__(self, key):
        if isinstance(key, int):
            return self._text[key]
        if isinstance(key, slice):
            start, stop, step = key.indices(len(self._text))
            return self._text[start:stop:step]
        raise TypeError("keyはintまたはslice")
Python

indicesは負の値や範囲外を安全な範囲に正規化してくれるため、実装が安定します。


シーケンス設計の型(len・負インデックス・不変条件)

lenを揃える(サイズの一貫性)

class Inventory:
    def __init__(self):
        self._set: set[str] = set()
    def add(self, sku: str):
        self._set.add(sku)
    def __len__(self) -> int:
        return len(self._set)
    def __getitem__(self, index: int) -> str:
        # setは順序がないので、listに変換して順序を決める
        return list(self._set)[index]
Python

len()が直感どおりの「要素数」を返すようにしておくと、インデックスとも整合します。順序のない集合をインデックスで扱う場合は、並びの定義(ソートなど)を明記するのが安全です。

負のインデックス・ステップの対応

Pythonの標準シーケンスは、-1が末尾、-2が末尾のひとつ前…と扱います。listや文字列に委譲する実装なら自然に対応できます。自前で配列を持つ場合も「index < 0 なら len + index」を使って負インデックスに対応するのが定石です。

不変条件(範囲外はIndexError、型不正はTypeError)

この2つは“空気のような契約”。守るだけで、forループ・in演算子・スライス連鎖など標準機能との相性が格段に良くなります。


応用と関連メソッド(setitem・delitem・イテレーション)

書き込み・削除にも対応する

class Store:
    def __init__(self):
        self._items: list[str] = []
    def __getitem__(self, i: int) -> str:
        return self._items[i]
    def __setitem__(self, i: int, v: str) -> None:
        if not v: raise ValueError("空不可")
        self._items[i] = v
    def __delitem__(self, i: int) -> None:
        del self._items[i]
Python

ミュータブルなシーケンスにしたいなら、setitemdelitem を合わせて実装します。スライスの書き込み・削除もサポートできます(listに委譲すると楽です)。

イテレーションとの関係

現代的には iter を実装するのが推奨ですが、古いプロトコル(0から順に getitem を呼び、IndexErrorで停止)でもforループが動きます。パフォーマンスと可読性のため、iterの実装を用意するのが良い設計です。

class Cart:
    def __init__(self):
        self._items = []
    def add(self, sku, qty):
        self._items.append((sku, qty))
    def __getitem__(self, i):
        return self._items[i]
    def __len__(self):
        return len(self._items)
    def __iter__(self):
        return iter(self._items)  # 標準の反復子を返す
Python

例題(テキスト行バッファと商品カートの堅牢なgetitem)

行バッファ:行数とスライスを自然に扱う

class TextLines:
    def __init__(self):
        self._lines: list[str] = []

    def add(self, line: str):
        if "\n" in line:
            raise ValueError("1行に改行は不可")
        self._lines.append(line)

    def __len__(self) -> int:
        return len(self._lines)

    def __getitem__(self, key):
        if isinstance(key, int):
            return self._lines[key]  # 負インデックスOK(listに委譲)
        if isinstance(key, slice):
            return self._lines[key]  # listをそのまま返す設計
        raise TypeError("keyはintまたはslice")
Python

カート:同型で部分カートを返す(スライスは新インスタンス)

class Cart:
    def __init__(self):
        self._items: list[tuple[str, int]] = []
    def add(self, sku: str, qty: int):
        if qty <= 0:
            raise ValueError("数量は正の数")
        self._items.append((sku, qty))
    def __len__(self) -> int:
        return len(self._items)
    def __getitem__(self, key):
        if isinstance(key, int):
            return self._items[key]
        if isinstance(key, slice):
            out = Cart()
            for sku, qty in self._items[key]:
                out.add(sku, qty)
            return out
        raise TypeError("keyはintまたはslice")
Python

この設計なら、c[1:4] の結果をそのまま別の処理に渡せるため、扱いやすさと安全性が両立します。


まとめ(getitemは「インデックスとスライスの入口」。標準の契約を守る)

getitemを実装すると、obj[i]・obj[a:b]・forループなど、Pythonらしい操作が自作クラスでも自然に使えます。範囲外はIndexError、型不正はTypeError、インデックスは0始まり、負インデックスとスライスに対応——この標準契約を守り、leniterと整合させる。スライスは同型で返すか、listで返すかを明確に決め、性能面では基盤構造(listや文字列)に委譲してO(1)〜O(n)の直感的なコストに保つ。これを徹底すれば、初心者でも「読みやすく、拡張しやすい」シーケンス設計を自然に書けるようになります。

タイトルとURLをコピーしました