Pythonにおける Predicate 設計
Pythonで「条件による判定」や「フィルタリング」処理をする predicate(T -> bool) の設計パターンについて考えます。
例題:User を条件でフィルタする
下記のような単純なクラスのリストについてフィルタする際の実装を例に考えます。
from dataclasses import dataclass
@dataclass
class User:
id: int
age: int
is_active: bool
関数(Callable)による predicate
from typing import Callable
UserPredicate = Callable[[User], bool]
def is_adult(user: User) -> bool:
return user.age >= 18
def is_active(user: User) -> bool:
return user.is_active
def filter_users(users: list[User], pred: UserPredicate) -> list[User]:
return [u for u in users if pred(u)]
and / orの拡張
def and_(*preds: UserPredicate) -> UserPredicate:
return lambda x: all(p(x) for p in preds)
def or_(*preds: UserPredicate) -> UserPredicate:
return lambda x: any(p(x) for p in preds)
eligible = and_(is_adult, is_active)
特徴
- 最小コストで書ける、テスト容易性
- ラムダ・高階関数と相性が良い
- 条件などに利用する状態を管理しようとすると辛い
ABC(抽象基底クラス)による predicate
from abc import ABC, abstractmethod
class UserPredicator(ABC):
@abstractmethod
def test(self, user: User) -> bool:
...
class Adult(UserPredicator):
def test(self, user: User) -> bool:
return user.age >= 18
class Active(UserPredicator):
def test(self, user: User) -> bool:
return user.is_active
def filter_users(users: list[User], pred: UserPredicator) -> list[User]:
return [u for u in users if pred.test(u)]
and / orの拡張
class And(UserPredicator):
def __init__(self, *preds: UserPredicator):
self.preds = preds
def test(self, user: User) -> bool:
return all(p.test(user) for p in self.preds)
class Or(UserPredicator):
def __init__(self, *preds: UserPredicator):
self.preds = preds
def test(self, user: User) -> bool:
return any(p.test(user) for p in self.preds)
eligible = And(Adult(), Active())
特徴
- 継承による統制が効く
- 状態・追加メソッドを持たせやすい
- 継承前提なので柔軟性はやや低い
Protocol による predicate
from typing import Protocol, TypeVar
T = TypeVar("T")
class Predicate(Protocol[T]):
def __call__(self, x: T) -> bool: ...
def is_adult(user: User) -> bool:
return user.age >= 18
class Active:
def __call__(self, user: User) -> bool:
return user.is_active
def filter_users(users: list[User], pred: UserPredicate) -> list[User]:
return [u for u in users if pred(u)]
and / or の拡張
def and_(*preds: Predicate[T]) -> Predicate[T]:
return lambda x: all(p(x) for p in preds)
def or_(*preds: Predicate[T]) -> Predicate[T]:
return lambda x: any(p(x) for p in preds)
eligible = and_(is_adult, Active())
特徴
- 関数 / lambda / callable class をすべて受けられる
- 継承不要(構造的部分型)
Protocol で Callable を定義する
Protocolで定義するメソッドにおいて下記のパターンがあります。(abcの抽象メソッドでも同様)
-
__call__(self, x: T) -> boolを定義してCallableにする -
predicate(self, x: T) -> boolなど普通に名前付きメソッドを定義する
それぞれの違い、特徴について考えます。
__call__ を定義する Protocol
class Predicate(Protocol[T]):
def __call__(self, x: T) -> bool: ...
特徴
- 関数・lambda をそのまま渡せる
- filter / any / all と自然に統合できる
- 「呼べる」という 振る舞い を表現
名前付きメソッドを定義する Protocol
class UserPredictor(Protocol):
def predicate(self, user: User) -> bool: ...
特徴
- 意図が明示的
- クラスとして振る舞いを追加など拡張がしやすい
- 関数を直接渡せない(ラッパーが必要)
Protocol による duck typing
duck typingとは「型や継承ではなく、使える振る舞いで判断する」
Pythonではこれを特殊メソッドが支えています。
__call__ を持つ → Callable
__iter__ を持つ → Iterable
これは「名前」ではなく言語が理解できる振る舞いです。
特殊メソッドは duck typing を言語として成立させるための共通語彙であり、
Protocol はそれを型レベルで宣言できるようにした仕組みです。
class Predicate(Protocol[T]):
def __call__(self, x: T) -> bool: ...
これは:
「この振る舞いができれば、この型として扱ってよい」
という宣言であり、
- ランタイムの自由さ(duck typing)
- 静的型検査の安全性
を両立します。
まとめ
PythonにおけるPredicate設計パターンとして、関数・ABC・Protocol を用いた定義を比較しました。
これらは排他的な選択肢ではなく、用途や成長段階に応じて使い分けられる設計手法です。
関数は最も軽量で、短く完結した判定ロジックに適しています。
ABCは継承を前提とした統制や拡張を必要とする場合に有効です。
Protocolは構造的部分型により、関数・クラスの両方を受け入れる境界を定義できます。
特に __call__ のような特殊メソッドを Protocol に定義することで、
「呼び出し可能である」という振る舞いを型として表現できます。
これはPythonのduck typingと整合しており、関数からcallableなクラスへの移行を破壊的変更なしに行える設計になります。
このように、Protocolと特殊メソッドを組み合わせることで、
実装の自由度を保ちながら型安全なインターフェースを定義できます。
Discussion