閉じる

【Python】オブジェクトにlen()やbool()を効かせる特殊メソッドの実装方法

Pythonのオブジェクトは、特定の「特殊メソッド」を実装するとlen()やbool()といった組み込み関数の対象にできます

この記事では、初心者でも失敗しにくい形で__len____bool__の実装手順や注意点を、動くサンプルとともに丁寧に解説します。

設計の意図や例外の挙動まで押さえることで、堅牢で読みやすいクラスを作れるようになります。

特殊メソッドと組み込み関数の関係

特殊メソッドとは

Pythonには__len____bool__など、名前に前後2つのアンダースコアを持つ特殊メソッドがあります。

これらは演算子や組み込み関数の裏側で自動的に呼ばれるフックで、クラスに定義すると、len(x)bool(x)の振る舞いをカスタマイズできます。

例えば、リストは内部的に__len__を実装しているため、len([1,2,3])が3を返せます。

組み込み関数(len, bool)が呼ぶ仕組み

組み込み関数は直接属性参照せず、プロトコルに従って特殊メソッドを解決します。

概念的には次のように動作します。

  • len(x)x.__len__() を探して呼び出す
  • bool(x) → まず x.__bool__()、なければ x.__len__() を呼ぶ

理解を助けるため、対応関係を表にまとめます。

組み込み関数優先して呼ばれる特殊メソッドフォールバック先備考
len(x)__len__なし返り値は非負のint必須
bool(x)__bool____len__長さ0はFalse、それ以外はTrue

この自動呼び出しにより、利用者は関数側のコードを書き換えずに拡張できます

呼び出し順序とフォールバック

bool(x)__bool__が未定義のとき__len__へフォールバックします。

__len__が0を返せばFalse、正の値を返せばTrueです。

一方、len(x)にフォールバックはありません

__len__未定義のオブジェクトにlen()を適用するとTypeErrorになります

この挙動を知っておくと、設計段階で「空のときは偽」といった自然な真偽値設計を行いやすくなります。

len()を効かせる__len__の実装方法

戻り値は非負のint

__len__は必ず非負のintを返す必要があります

負の値を返すとValueError、整数以外を返すとTypeErrorになります。

特にTrue/Falseintのサブクラスのため受理されますが、誤解を招くので避けましょう

O(1)が理想の設計

長さは毎回の呼び出しで即座に返せる(O(1))のが理想です。

コレクションのサイズを都度走査すると、len()が性能ボトルネックになります。

内部状態として要素数(例えば_size)を持ち、更新時に同期させるのが典型的です。

長さ0の意味と使い分け

長さ0は「空」を表し、bool(x)Falseに評価されます

これによりif not x:のような自然な条件分岐が書けます。

一方で、「未初期化」と「空」を区別したい設計では、__bool__を独自に実装し、長さ0でもTrueにするなどの選択もあり得ます。

ただし、一般には空=偽の一貫性が他者にとって理解しやすいです。

サンプル: 独自クラスに__len__を定義

以下は、要素数をO(1)で返す簡易コンテナです。

Pythonlen_protocol_example.py
# len_protocol_example.py

from collections.abc import Iterable

class Bag:
    """重複を許す簡易コンテナ。len()対応のため__len__を実装。"""
    def __init__(self, items: Iterable = ()):
        # 内部リストとサイズを同期して保持
        self._items = list(items)
        self._size = len(self._items)  # O(1)にするためキャッシュ
        # __len__は非負のintを返すべき。_sizeは常に0以上を保証する。

    def add(self, item) -> None:
        self._items.append(item)
        self._size += 1  # サイズを同期

    def remove_one(self, item) -> None:
        """最初に見つかった1個だけ削除。存在しない場合はValueError。"""
        idx = self._items.index(item)  # 見つからないとValueError
        del self._items[idx]
        self._size -= 1

    def __len__(self) -> int:
        """非負のintをO(1)で返す。副作用なし。"""
        return self._size

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


if __name__ == "__main__":
    bag = Bag(["apple", "banana"])
    print(bag, len(bag))  # 2
    bag.add("apple")
    print(bag, len(bag))  # 3
    bag.remove_one("banana")
    print(bag, len(bag))  # 2

    # エラー例: __len__が負にならない設計のため通常は起きないが、
    # もし負を返せば ValueError、int以外なら TypeError になる。
実行結果
Bag(['apple', 'banana']) 2
Bag(['apple', 'banana', 'apple']) 3
Bag(['apple', 'apple']) 2

ポイントは__len__を副作用なし・即時応答に保つことです。

bool()を効かせる__bool__の実装方法

__bool__が無い場合は__len__を参照

__bool__未定義ならbool(x)__len__へフォールバックします。

len(x) == 0ならFalse、それ以外はTrueです。

このため、コレクション型では__len__だけ実装しても自然な真偽値判定が機能します。

戻り値はTrueかFalseのみ

__bool__TrueFalseのみを返すべきです。

技術的には他の値も真偽値に解釈されますが、可読性と一貫性のため厳格にboolを返すのが推奨です。

Truthy/Falseyの設計指針

オブジェクトの真偽は、利用者が「何をもって意味のある状態とみなすか」を反映します。

一般的な指針は次の通りです。

  • コレクション: 空ならFalse、要素ありならTrue
  • リソース状態(接続・初期化): 有効ならTrue、無効ならFalse
  • 計算結果ラッパー: 成功ならTrue、失敗ならFalse

いずれも条件分岐で直感的に読めることを優先します。

サンプル: 条件分岐で使えるクラス

接続状態を真偽で表すオブジェクトの例です。

長さの概念がないため__bool__を実装します。

Pythonbool_protocol_example.py
# bool_protocol_example.py

class Connection:
    """疑似的な接続オブジェクト。接続中なら真。"""
    def __init__(self) -> None:
        self._connected = False

    def connect(self) -> None:
        # 実際はソケットやDB接続などを行う
        self._connected = True

    def close(self) -> None:
        self._connected = False

    def __bool__(self) -> bool:
        """副作用なしで現在の有効性を返す。"""
        return self._connected  # True/Falseのみを返す

    def __repr__(self) -> str:
        state = "connected" if self._connected else "closed"
        return f"<Connection {state}>"


if __name__ == "__main__":
    conn = Connection()
    if conn:
        print("これは表示されない")
    else:
        print("未接続です")

    conn.connect()
    if conn:
        print("接続中です")

    conn.close()
    print(bool(conn))  # False
実行結果
未接続です
接続中です
False

真偽値の意味がクラス名や役割と一致していることが、読みやすいAPIにつながります。

実装のチェックリストと注意点

不正な戻り値でのTypeErrorに注意

__len__は非負のint、__bool__はboolを返すことを徹底します。

以下は、誤った実装例とその結果の一部です。

Pythonwrong_returns.py
# wrong_returns.py

class BadLen:
    def __len__(self):
        return -1  # 負の値 -> ValueError

class BadLenType:
    def __len__(self):
        return "3"  # 文字列 -> TypeError

class BadBool:
    def __bool__(self):
        return 2  # bool以外(技術的には解釈される可能性があるが非推奨)

if __name__ == "__main__":
    try:
        len(BadLen())
    except Exception as e:
        print(type(e).__name__, e)

    try:
        len(BadLenType())
    except Exception as e:
        print(type(e).__name__, e)

    # BadBoolは環境により真として解釈され得るが、設計として誤り
    print(bool(BadBool()))
実行結果
ValueError __len__() should return >= 0
TypeError 'str' object cannot be interpreted as an integer
True

返り値の型・範囲は必ず守りましょう

副作用のない実装にする

特殊メソッドは観察メソッドとして実装し、副作用を持たせないことが重要です。

__len____bool__内でログ出力、ネットワーク呼び出し、状態変更を行うと、if x:len(x)の実行で予期せぬ挙動が起きます。

デバッグは呼び出し側で行う、あるいは明示的メソッド(例: is_connected())を用意して切り分けると安全です。

テスト観点と例外の確認

初心者でも取り組みやすい観点を挙げます。

多用は避けつつ、要点のみ列挙します。

  • 正常系: 空/非空のときlen()bool()が期待通りか
  • 例外系: __len__が負値や非intを返したときValueError/TypeErrorになるか
  • 境界値: 0、1、非常に大きい数での動作
  • 性能: len()の呼び出しがO(1)であるか(大きなデータで体感確認)
  • 一貫性: __bool____len__の意味が矛盾していないか

簡易テスト例:

下記サンプルを動かす場合は、少し前に掲載したサンプル len_protocol_example.pyを同じフォルダに配置しておいてください。

Pythonquick_tests.py
# quick_tests.py

def assert_len_and_bool_empty(obj):
    assert len(obj) == 0
    assert bool(obj) is False  # __bool__未定義なら__len__へフォールバック

def assert_len_and_bool_nonempty(obj, expected_len):
    assert len(obj) == expected_len
    assert bool(obj) is True

if __name__ == "__main__":
    from len_protocol_example import Bag

    empty = Bag()
    assert_len_and_bool_empty(empty)

    bag = Bag(["x", "y"])
    assert_len_and_bool_nonempty(bag, 2)

    print("OK")
実行結果
OK

テストで「意味」「型」「境界」「例外」をセットで確認する習慣が、品質を大きく高めます。

まとめ

len()とbool()をオブジェクトに効かせるには、対応する特殊メソッドの正しい実装が鍵です。

__len__非負のintをO(1)で返すことを目標にし、__bool__True/Falseのみを返す方針を守ると、読みやすく直感的なAPIになります。

空=偽という一貫性を基本にしつつ、必要に応じて__bool__で意味を明示しましょう。

最後に、副作用を持たせない戻り値の型・範囲を守るテストで例外や境界を確認するという原則を押さえれば、実務でも安心して使える堅牢なクラス設計ができます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

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

URLをコピーしました!