概要
あるプロトコルを満たすオブジェクトを受け取り、そのオブジェクトを返す関数を考える。この関数の引数及び戻り値の型は、プロトコルクラスそのものより、プロトコルクラスを上界としたジェネリック型にしておくと都合が良い。
具体例
次のようなプロトコルを使ってソート関数を定義する。
ソート関数は破壊的で、利便性のためにソート結果を返すとする。
class Sortable(Protocol):
def __len__(self) -> int:
...
def swap(self, i: int, j: int) -> None:
...
def less(self, i: int, j: int) -> bool:
...
def sort(c: Sortable) -> Sortable:
...
return c
この時、上のソート関数のように戻り値の型がプロトコルクラスになっていると、受け取ったオブジェクトに対する操作が制限されてしまう。
例えば、次のようなget
メソッドを持つオブジェクトをソートすると、戻り値の型がSortable
なのでget
メソッドが見つからない。
class WeightedList:
def __len__(self) -> int:
...
def swap(self, i: int, j: int) -> None:
...
def less(self, i: int, j: int) -> bool:
...
def get(self, i: int) -> str:
...
items = WeightedList()
...
sort(items).get(10) # -> "Sortable" has no attribute “get”
もちろん今回の場合は二行に分けて、
sort(items)
items.get(10)
などとすれば問題ないのだが、lambda式の中では使えない。
解決策
戻り値の型をプロトコルクラスではなく、受け取ったオブジェクトの型と一致させれば良い。
つまりプロトコルを満たすジェネリック型を受け取り同じ型を返す関数として定義する。
T = TypeVar("T", bound=Sortable)
def sort(c: T) -> T:
...
return c
items = WeightedList()
...
sort(items).get(10) # sort(items)は WeightedList を返すので get を使える
デメリットとしては、型変数の名前をシンプルなT
などとしていると、何を受け取る関数なのか自明でなくなってしまう。
「Sortable
を継承している任意の型」と一目で分かる良い名前があれば良いのだけれど・・・。
ところで
このTypeVar
におけるbound
の使い方は、ドキュメントには説明されていません。
TypeVar
の項には、
a type variable may specify an upper bound using bound=
<type>
. This means that an actual type substituted (explicitly or implicitly) for the type variable must be a subclass of the boundary type, see PEP 484.
と書かれていて、「an actual type ... must be a subclass of the boundary type」今回の例だとWeightedList
はSortable
のサブクラスである必要があるように読めます。
厳密に言えば、WeightedList
はSortable
の構造的部分型(structural subtyping)であってサブクラスではありません。
エラーになるかと思いましたが、mypyではエラーになりませんでした。
ついでなので、PEP 484 の該当部分を見てみると、
A type variable may specify an upper bound using bound=
<type>
(note:<type>
itself cannot be parameterized by type variables). This means that an actual type substituted (explicitly or implicitly) for the type variable must be a subtype of the boundary type.
となっていました。「an actual type ... must be a subtype of the boundary type」つまり構造的部分型でも問題ないようです。