2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Python: プロトコルクラスの代わりにジェネリック型を返す

Last updated at Posted at 2022-03-22

概要

あるプロトコルを満たすオブジェクトを受け取り、そのオブジェクトを返す関数を考える。この関数の引数及び戻り値の型は、プロトコルクラスそのものより、プロトコルクラスを上界としたジェネリック型にしておくと都合が良い。

具体例

次のようなプロトコルを使ってソート関数を定義する。
ソート関数は破壊的で、利便性のためにソート結果を返すとする。

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などとしていると、何を受け取る関数なのか自明でなくなってしまう。
pycharm.png
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」今回の例だとWeightedListSortableのサブクラスである必要があるように読めます。

厳密に言えば、WeightedListSortable構造的部分型(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」つまり構造的部分型でも問題ないようです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?