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
/False
はint
のサブクラスのため受理されますが、誤解を招くので避けましょう。
O(1)が理想の設計
長さは毎回の呼び出しで即座に返せる(O(1))のが理想です。
コレクションのサイズを都度走査すると、len()
が性能ボトルネックになります。
内部状態として要素数(例えば_size
)を持ち、更新時に同期させるのが典型的です。
長さ0の意味と使い分け
長さ0は「空」を表し、bool(x)
でFalse
に評価されます。
これによりif not x:
のような自然な条件分岐が書けます。
一方で、「未初期化」と「空」を区別したい設計では、__bool__
を独自に実装し、長さ0でもTrue
にするなどの選択もあり得ます。
ただし、一般には空=偽の一貫性が他者にとって理解しやすいです。
サンプル: 独自クラスに__len__を定義
以下は、要素数をO(1)で返す簡易コンテナです。
# 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__
はTrue
かFalse
のみを返すべきです。
技術的には他の値も真偽値に解釈されますが、可読性と一貫性のため厳格にboolを返すのが推奨です。
Truthy/Falseyの設計指針
オブジェクトの真偽は、利用者が「何をもって意味のある状態とみなすか」を反映します。
一般的な指針は次の通りです。
- コレクション: 空ならFalse、要素ありならTrue
- リソース状態(接続・初期化): 有効ならTrue、無効ならFalse
- 計算結果ラッパー: 成功ならTrue、失敗ならFalse
いずれも条件分岐で直感的に読めることを優先します。
サンプル: 条件分岐で使えるクラス
接続状態を真偽で表すオブジェクトの例です。
長さの概念がないため__bool__
を実装します。
# 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を返すことを徹底します。
以下は、誤った実装例とその結果の一部です。
# 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を同じフォルダに配置しておいてください。
# 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__
で意味を明示しましょう。
最後に、副作用を持たせない、戻り値の型・範囲を守る、テストで例外や境界を確認するという原則を押さえれば、実務でも安心して使える堅牢なクラス設計ができます。