閉じる

Pythonのコンテナ型をゼロから自作する方法|リスト風クラスを実装

Pythonで標準のlistに慣れてくると、「自分でコンテナ型を作れたら便利そう」と感じる場面が増えてきます。

この記事では、Pythonのコンテナ型をゼロから自作し、リスト風クラスを実際に実装しながら、設計の考え方や発展テクニックまで丁寧に解説します。

標準listとの違いを通して、データ構造・抽象化・パフォーマンスの勘所も身につけていきましょう。

コンテナ型を自作するメリット

標準リスト(list)との違いを理解する意味

Pythonのlistは非常に高機能で最適化もされていますが、その内部構造や動作原理は普段の利用だけでは見えにくいものです。

自分で「リスト風のコンテナ」を作ってみると、標準listがどのような設計思想で作られているかを自然と意識するようになります。

標準listと自作コンテナの関係を、整理しておきます。

観点標準list自作コンテナ
主目的汎用的な利用特定用途向け・学習
制約ほぼなし任意の制約を追加可能
速度非常に高速実装次第(たいてい遅い)
柔軟性APIは固定必要なAPIを設計できる
学習効果使い方を学ぶ仕組みと設計を学ぶ

標準listで十分な場面が大半ですが、あえて自作することで「なぜlistはこの振る舞いなのか」「このメソッドはどのように実装されうるのか」を具体的に理解できるようになります。

データ構造とアルゴリズム学習としてのコンテナ自作

コンテナ型の自作は、単にクラスを書くという作業にとどまりません。

実際には、データ構造・アルゴリズム・インターフェース設計を総合的に扱うトレーニングになります。

例えば、次のような観点が自然と身につきます。

文章で言い換えると、配列リスト、リンクリスト、スタック、キューといった定番のデータ構造が、どのような操作でコストが高くなるのか、あるいは安くなるのかを実感できます。

appendやinsertの挙動を自分で書いてみることで、「先頭への挿入がなぜ遅くなりがちなのか」といった疑問にも具体的な感覚が伴うようになります。

実務コードに活きる設計力・抽象化のトレーニング

実務では、次のような要件に直面することがよくあります。

例えば「必ずID順にソートされた一覧を保持したい」「負の値を絶対に入れてはいけない」などのルールがあるコレクションを扱うとき、単なるlistにif文を散りばめて対応すると、コードベースがすぐに読みにくくなります。

そこで、自作のコンテナを用いてルールや制約を一箇所に閉じ込めると、ビジネスロジック側のコードは非常にシンプルになります。

これはドメイン駆動設計(DDD)などでもよく使われる考え方で、コンテナ自作の経験はそうした設計スタイルを身につけるのにも役立ちます。

コンテナ型の基本と前提知識

コンテナ型(Container)とは何かを整理

Pythonにおけるコンテナ型とは、「複数の要素をまとめて保持し、要素へのアクセス手段を提供するオブジェクト」のことです。

代表例として次のような型があります。

文章で述べると、listやtupleのように順序を持ったもの、setのように順序よりも集合としての計算を重視するもの、dictのようにキーと値のペアを扱うものなど、形はさまざまですが、「中に複数の要素を持ち、それに対する基本操作を提供する」という点は共通です。

コンテナ型の特徴としては、lenで要素数を取得できることや、for文で要素を順に取り出せること、in演算子で要素の存在確認ができることなどが挙げられます。

シーケンス型とミュータブル型の違い

コンテナ型を整理するときにまず意識したいのがシーケンス型かどうかミュータブルかイミュータブルかという2つの軸です。

シーケンス型とは、要素に順序があり、通常はインデックスでアクセスできるコンテナを指します。

listやtuple、strなどがこれに該当します。

一方で、dictやsetは順序は扱いますが「シーケンス」というよりは「マッピング」「集合」として分類されます。

ミュータブルかイミュータブルかは、生成後に中身を変更できるかどうかの違いです。

listやdictはミュータブルであり、要素の追加や削除が可能です。

対してtupleやstrはイミュータブルであり、一度作ったものを直接書き換えることはできません。

この記事で作るのは、順序を持ちインデックスアクセスができ、かつ中身を変更可能な「ミュータブルなシーケンス型」です。

つまり、Pythonのlistに近い立ち位置のコンテナになります。

collections.abcによるコンテナのプロトコル

Pythonにはcollections.abcというモジュールがあり、そこでコンテナに関する抽象基底クラス(ABC)が定義されています。

これらは「このインターフェースを持っていれば、この種のコンテナだとみなせる」というプロトコルを規定しています。

代表的なものをいくつか挙げます。

抽象基底クラス役割
Container__contains__を持つもの
Sized__len__を持つもの
Iterable__iter__を持つもの
Sequence読み取り専用のシーケンス
MutableSequence変更可能なシーケンス

MutableSequenceを継承して必要なメソッドを実装すると、「シーケンスとしての最低限のふるまい」を自動的に満たすことができます。

この記事の後半では、このMutableSequenceを活用した実装も紹介します。

自作コンテナで実装すべき主要メソッド一覧

リスト風コンテナとして使えるようにするためには、最低限次のようなメソッド(特殊メソッドを含む)を意識する必要があります。

ここでは概要だけ挙げておき、後のセクションで実装していきます。

  • 基本的なインターフェースとして、要素数を返す__len__、インデックスアクセスを提供する__getitem__が重要です。これだけでかなり「コンテナらしく」なります。
  • 更新に関する操作として、要素を置き換える__setitem__、削除する__delitem__、末尾に要素を追加するappend、任意の位置へのinsertなどがあります。
  • 反復とユーティリティの面では、for文に対応する__iter__、in演算子に対応する__contains__、デバッグ時に読みやすい文字列表現を返す__repr__なども実装しておくと使い勝手が格段に良くなります。

リスト風クラスの基本実装

クラス設計方針とインターフェースの決定

この記事では、まず内部実装にPythonのlistをそのまま利用し、その上に「リスト風の皮」をかぶせる方針で実装します。

低レベルなメモリ管理から始めるのではなく、「インターフェース設計」と「特殊メソッドの役割」に焦点を当てる形です。

最初の段階では、次のような要件を満たすSimpleListクラスを目指します。

  • listのように初期値を与えて生成できること
  • len()で要素数を取得できること
  • インデックスで要素取得・代入・削除ができること
  • appendinsertで要素の追加ができること
  • for文やin演算子で自然に扱えること
  • printしたときに中身が分かりやすいこと

__init__で内部配列(リスト)を保持する

まずは一番基本である__init__を実装し、内部にPythonのlistを保持するようにします。

Python
class SimpleList:
    """Pythonのlist風の基本コンテナクラス"""

    def __init__(self, iterable=None):
        """初期化メソッド
        
        Args:
            iterable: 初期要素を与えるための反復可能オブジェクト
                      例) [1, 2, 3], range(5), ("a", "b")
        """
        if iterable is None:
            # 引数がない場合は空のリストを内部に持つ
            self._items = []
        else:
            # 渡されたiterableからlistを作成して内部に保持
            # こうすることで、外部から渡されたリストを
            # 直接共有せずに「コピー」として持つことができます
            self._items = list(iterable)

この段階では、まだコンテナらしいふるまいはほとんどしませんが、「内部に本物のlistを隠して持つ」という構造だけは明確になりました

__len__と__getitem__で最小限のコンテナ化

コンテナらしさを一気に高めるのが、__len____getitem__の実装です。

これによりlen()関数と[]による読み取りアクセスが使えるようになります。

Python
class SimpleList:
    """Pythonのlist風の基本コンテナクラス"""

    def __init__(self, iterable=None):
        if iterable is None:
            self._items = []
        else:
            self._items = list(iterable)

    def __len__(self):
        """len(self) が呼び出されたときの挙動を定義"""
        # 内部リストの長さをそのまま返します
        return len(self._items)

    def __getitem__(self, index):
        """self[index] で要素を取得するときの挙動を定義"""
        # 内部リストに処理を委譲します
        return self._items[index]

実際に簡単な動作を確認してみましょう。

Python
s = SimpleList([10, 20, 30])
print(len(s))      # __len__ が呼ばれる
print(s[1])        # __getitem__ が呼ばれる
print(s[0:2])      # スライスも一応動く(後で改良します)
実行結果
3
20
[10, 20]

今の__getitem__は内部listにそのまま委譲しているため、スライスを使うとPythonのlistが直接返ってくる点には注意が必要です。

後ほど「スライス対応」のセクションで、ここを自作コンテナのインスタンスを返すように改善します。

__setitem__と__delitem__で更新と削除を実装

リスト風コンテナとして使うには、中身の更新と削除もできなければ不便です。

そこで__setitem____delitem__を追加します。

Python
class SimpleList:
    """Pythonのlist風の基本コンテナクラス"""

    def __init__(self, iterable=None):
        if iterable is None:
            self._items = []
        else:
            self._items = list(iterable)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

    def __setitem__(self, index, value):
        """self[index] = value の挙動を定義"""
        # ここでも内部リストに処理を丸投げします
        self._items[index] = value

    def __delitem__(self, index):
        """del self[index] の挙動を定義"""
        # 対象の要素を削除します
        del self._items[index]

挙動を確認してみます。

Python
s = SimpleList([1, 2, 3, 4])
s[1] = 20       # __setitem__ が動く
del s[2]        # __delitem__ が動く

print(s._items)  # 実験なので内部を直接参照
実行結果
[1, 20, 4]

このように、特殊メソッドを実装しておくと、通常の構文(s[1] = x や del s[2])が自然に自作クラスにも適用されることが分かります。

appendとinsertで要素追加をサポート

listで頻繁に使うappendinsertも、自作コンテナに実装しておくと便利です。

実装自体は非常にシンプルで、内部listに処理を委譲するだけです。

Python
class SimpleList:
    """Pythonのlist風の基本コンテナクラス"""

    def __init__(self, iterable=None):
        if iterable is None:
            self._items = []
        else:
            self._items = list(iterable)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

    def __setitem__(self, index, value):
        self._items[index] = value

    def __delitem__(self, index):
        del self._items[index]

    def append(self, value):
        """末尾に要素を追加します"""
        self._items.append(value)

    def insert(self, index, value):
        """指定した位置に要素を挿入します"""
        self._items.insert(index, value)

動作確認をしてみます。

Python
s = SimpleList()
s.append(10)
s.append(20)
s.insert(1, 15)

print(s._items)
実行結果
[10, 15, 20]

このようにまずは内部listに処理を任せるだけで、かなりlistに近いインターフェースを持ったクラスを作ることができます。

__iter__と__contains__で反復処理に対応

コンテナをコンテナたらしめているのは、forループで回したり、in演算子で要素の存在確認ができることです。

これを実現するのが__iter____contains__です。

Python
class SimpleList:
    """Pythonのlist風の基本コンテナクラス"""

    def __init__(self, iterable=None):
        if iterable is None:
            self._items = []
        else:
            self._items = list(iterable)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

    def __setitem__(self, index, value):
        self._items[index] = value

    def __delitem__(self, index):
        del self._items[index]

    def append(self, value):
        self._items.append(value)

    def insert(self, index, value):
        self._items.insert(index, value)

    def __iter__(self):
        """for文での反復処理に対応させます"""
        # 内部リストのイテレータをそのまま返します
        return iter(self._items)

    def __contains__(self, value):
        """in演算子の挙動を定義します"""
        # value が内部リストに含まれているかを調べます
        return value in self._items

挙動を確認します。

Python
s = SimpleList([1, 2, 3])

for x in s:
    print(x, end=" ")

print("\n2 in s:", 2 in s)
print("99 in s:", 99 in s)
実行結果
1 2 3 
2 in s: True
99 in s: False

__iter__と__contains__を実装することで、「Python的な書き方」で自然に扱えるクラスになってきました。

__repr__でデバッグしやすい表示を実装

最後に、デバッグ時に中身を確認しやすくするため、__repr__を実装します。

reprは、インタプリタ上で変数を評価したときや、printでオブジェクトを表示したときに呼び出される「代表表現」を定義するメソッドです。

Python
class SimpleList:
    """Pythonのlist風の基本コンテナクラス"""

    def __init__(self, iterable=None):
        if iterable is None:
            self._items = []
        else:
            self._items = list(iterable)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

    def __setitem__(self, index, value):
        self._items[index] = value

    def __delitem__(self, index):
        del self._items[index]

    def append(self, value):
        self._items.append(value)

    def insert(self, index, value):
        self._items.insert(index, value)

    def __iter__(self):
        return iter(self._items)

    def __contains__(self, value):
        return value in self._items

    def __repr__(self):
        """開発者向けのわかりやすい文字列表現を返します"""
        # クラス名と内部リストのreprを組み合わせます
        return f"{self.__class__.__name__}({self._items!r})"

動作確認です。

Python
s = SimpleList([1, 2, 3])
print(s)       # __repr__ が使われる
s              # 対話モードではこれも __repr__
実行結果
SimpleList([1, 2, 3])
SimpleList([1, 2, 3])

これで最小限ながら「リスト風に見えるコンテナ」が完成しました。

次の章からは、これを発展させて実務でも使えるレベルのコンテナ設計を目指します。

コンテナ自作を発展させる実装テクニック

バリデーション付きリスト風クラスの実装

実務で役立つ自作コンテナの典型例が、要素に対して特定の制約をかける「バリデーション付きリスト」です。

例えば、「整数のみ受け付けるリスト」「0以上100以下の値しか入れないリスト」などが考えられます。

ここでは例として、整数以外は入れられないコンテナIntListを実装してみます。

Python
class IntList(SimpleList):
    """整数のみを格納できるリスト風コンテナ"""

    def _validate(self, value):
        """内部用のバリデーションメソッド
        
        value が int でない場合は TypeError を送出します。
        """
        if not isinstance(value, int):
            raise TypeError(f"IntList には int 型のみ格納できます: {value!r}")

    def append(self, value):
        """要素追加のたびにバリデーションを行います"""
        self._validate(value)
        super().append(value)

    def insert(self, index, value):
        """挿入時もバリデーションを行います"""
        self._validate(value)
        super().insert(index, value)

    def __setitem__(self, index, value):
        """代入時のバリデーションも忘れずに"""
        self._validate(value)
        super().__setitem__(index, value)

動作確認をしてみます。

Python
nums = IntList([1, 2, 3])
nums.append(4)      # OK
nums[0] = 10        # OK

print(nums)

try:
    nums.append("not-int")  # エラーになるはず
except TypeError as e:
    print("エラー:", e)
実行結果
SimpleList([10, 2, 3, 4])
エラー: IntList には int 型のみ格納できます: 'not-int'

このようにバリデーションロジックをコンテナに閉じ込めることで、呼び出し元のコードは「常に整数だけが入っている」という前提で書けるようになります。

これは実務の品質向上にも直結するテクニックです。

イミュータブルなリスト風コンテナ(ReadOnlyList)の作成

次に、読み取り専用のリスト風コンテナReadOnlyListを作ります。

これは、外部に配列を渡したいが、中身を書き換えられては困るような状況で役立ちます。

実装方針としては、__getitem____len__など読み取り系はそのまま許可し、__setitem__appendなど更新系メソッドでは例外を送出する形にします。

Python
class ReadOnlyList(SimpleList):
    """読み取り専用のリスト風コンテナ"""

    # 変更系操作で送出する例外クラスを1か所にまとめておくと便利です
    _error_message = "ReadOnlyList は変更できません"

    def __setitem__(self, index, value):
        raise TypeError(self._error_message)

    def __delitem__(self, index):
        raise TypeError(self._error_message)

    def append(self, value):
        raise TypeError(self._error_message)

    def insert(self, index, value):
        raise TypeError(self._error_message)

挙動を確認します。

Python
r = ReadOnlyList([1, 2, 3])
print(r[0], len(r))   # 読み取りはOK

try:
    r.append(4)
except TypeError as e:
    print("append 失敗:", e)

try:
    r[0] = 10
except TypeError as e:
    print("代入失敗:", e)
実行結果
1 3
append 失敗: ReadOnlyList は変更できません
代入失敗: ReadOnlyList は変更できません

このように「不変オブジェクトとして扱いたいコンテナ」を自作できると、状態が勝手に変わってしまうことによるバグを防ぎやすくなります

スライス対応

先ほど触れたように、現状の__getitem__ではスライスを行うとPythonのlistが返ってきます。

リスト風コンテナとしては、スライスも同じコンテナ型で返ってきてほしいところです。

スライスはsliceオブジェクトとして渡されるので、それを判定して挙動を切り替えます。

Python
class SimpleList:
    """Pythonのlist風の基本コンテナクラス(スライス対応版)"""

    def __init__(self, iterable=None):
        if iterable is None:
            self._items = []
        else:
            self._items = list(iterable)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        """インデックスまたはスライスで要素を取得"""
        result = self._items[index]
        # スライスの場合は list が返ってくるので、
        # 同じクラスのインスタンスに包み直します。
        if isinstance(index, slice):
            return self.__class__(result)
        return result

    def __setitem__(self, index, value):
        self._items[index] = value

    def __delitem__(self, index):
        del self._items[index]

    def append(self, value):
        self._items.append(value)

    def insert(self, index, value):
        self._items.insert(index, value)

    def __iter__(self):
        return iter(self._items)

    def __contains__(self, value):
        return value in self._items

    def __repr__(self):
        return f"{self.__class__.__name__}({self._items!r})"

動作確認です。

Python
s = SimpleList([10, 20, 30, 40, 50])

sub = s[1:4]      # スライス
print(sub, type(sub))

print(s[2])       # 通常のインデックスアクセス
実行結果
SimpleList([20, 30, 40]) <class '__main__.SimpleList'>
30

このようにスライスでも自作コンテナのインスタンスが返るようにすることで、より「一貫性のあるAPI」になります。

必要に応じて、__setitem__側でもスライス代入をカスタマイズすることができます。

継承でcollections.abc.MutableSequenceを利用する方法

ここまでゼロからメソッドを実装してきましたが、Python標準のcollections.abc.MutableSequenceを継承すると、いくつかのメソッドを自分で書かなくても済むようになります。

MutableSequenceを継承する場合に、基本的に自分で実装する必要があるのは次のメソッド群です。

  • __len__
  • __getitem__
  • __setitem__
  • __delitem__
  • insert

これらを実装しておけば、MutableSequence側でappendremoveなどを自動で提供してくれます。

簡単な例を示します。

Python
from collections.abc import MutableSequence

class ABCList(MutableSequence):
    """MutableSequence を利用したリスト風コンテナ"""

    def __init__(self, iterable=None):
        self._items = [] if iterable is None else list(iterable)

    # 必須: サイズ
    def __len__(self):
        return len(self._items)

    # 必須: 読み取り
    def __getitem__(self, index):
        return self._items[index]

    # 必須: 書き込み
    def __setitem__(self, index, value):
        self._items[index] = value

    # 必須: 削除
    def __delitem__(self, index):
        del self._items[index]

    # 必須: 挿入
    def insert(self, index, value):
        self._items.insert(index, value)

    def __repr__(self):
        return f"{self.__class__.__name__}({self._items!r})"

これだけで append や extend, pop などが自動的に使えるようになります。

Python
a = ABCList([1, 2, 3])
a.append(4)   # MutableSequence が提供
a.pop()       # これも自動提供
print(a)
実行結果
ABCList([1, 2, 3])

MutableSequenceを使うことで、自作コンテナのインターフェースが「Python標準の期待するシーケンスAPI」に自然と揃う点も大きな利点です。

型ヒントとジェネリクスで汎用コンテナ型を定義

Pythonの型ヒント(typing)を活用すると、「このコンテナにはどんな型の要素が入るのか」を静的に表現できるようになります。

特に、ジェネリクス(Generic)を使えば、要素型をパラメータとして持つコンテナクラスを定義できます。

ここでは、typing.GenericTypeVarを使った、ジェネリックなリスト風コンテナの雰囲気を示します。

Python
from typing import Generic, TypeVar, Iterable, Iterator

T = TypeVar("T")  # 要素型を表す型変数

class TypedList(Generic[T]):
    """型パラメータ付きのリスト風コンテナ"""

    def __init__(self, iterable: Iterable[T] | None = None):
        self._items: list[T] = [] if iterable is None else list(iterable)

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

    def __getitem__(self, index: int) -> T:
        return self._items[index]

    def __setitem__(self, index: int, value: T) -> None:
        self._items[index] = value

    def __delitem__(self, index: int) -> None:
        del self._items[index]

    def append(self, value: T) -> None:
        self._items.append(value)

    def __iter__(self) -> Iterator[T]:
        return iter(self._items)

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self._items!r})"

このクラスは、使用時に次のように型引数を指定して使えます。

Python
ints = TypedList[int]([1, 2, 3])
ints.append(4)

names = TypedList[str](["Alice", "Bob"])
names.append("Charlie")

print(ints)
print(names)
実行結果
TypedList([1, 2, 3, 4])
TypedList(['Alice', 'Bob', 'Charlie'])

型チェッカー(mypyやpyrightなど)を使えば、例えばTypedList[int]に文字列を渡したときに静的に警告を出すことができます。

「どんな値が入るコンテナなのか」をコードレベルで明示できるのは、規模が大きいプロジェクトほど効果的です。

パフォーマンスとメモリ使用量の考え方

最後に、コンテナ自作とパフォーマンス・メモリ使用量の関係について触れておきます。

自作コンテナは、内部にさらにlistや他のオブジェクトを保持する分、どうしても標準listより遅く・重くなりがちです。

特に、次のようなポイントに注意が必要です。

文章で整理すると、コンテナの設計では「1要素あたりどれくらいのオーバーヘッドを許容するか」を意識することが重要です。

例えば数百万要素を扱うような処理であれば、1要素ごとの余計なオブジェクトや関数呼び出しはすぐに全体のコストに跳ね返ってきます。

一方で、アプリケーション全体の中では、安全性・表現力・保守性のためのオーバーヘッドが十分に許容されることも多くあります。

とくにビジネスロジックを表現するレイヤーでは、少しの効率よりも「間違えにくさ」「意図の明確さ」を優先した設計が有効です。

最終的には、次のような指針で考えるとバランスが取りやすくなります。

  • パフォーマンスがシビアな箇所では、標準listや低レベルな配列を直接使う。
  • ビジネスロジックやルール表現が重要な箇所では、自作コンテナで抽象化と制約を明示する。
  • プロファイリングを用いて、ボトルネックになっている箇所だけを最適化する。

まとめ

この記事では、Pythonのコンテナ型を理解するために、内部にlistを保持するリスト風クラスをゼロから実装する流れを追いながら、バリデーション付きコンテナや読み取り専用コンテナ、スライス対応、collections.abc.MutableSequenceの活用、型ヒントによるジェネリックコンテナなど、実務にもつながる発展的なテクニックを紹介しました。

自作コンテナを通じて、データ構造や特殊メソッドの役割、設計と抽象化の感覚がつかめるようになると、標準listを使う場面でも「なぜこう設計されているのか」を意識できるようになります。

ぜひこの記事のコードをベースに、自分のプロジェクトの要件に合わせた独自コンテナを試しながら、Pythonと設計力の両方を一段深く理解していってください。

コーディングテクニック

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!