Python のジェネリクス

オブジェクトが持っている属性の型についても記述することができる型です。

例 リスト

例えば、リストについて考えてみます。あるリストは要素に int を持っていますし、あるリストは要素に str を持っています。

lst0 = [0, 1, 2]
lst1 = ['a', 'b', 'c']

例えば、そのままだと「list 型」としか言えなかったものが...

lst0: list = [0, 1, 2]
lst1: list = ['a', 'b', 'c']

ジェネリクスを使うと 「中身は int 型の List 型」 と言えるようになります。

from typing import List
lst0: List[int] = [0, 1, 2]
lst1: List[str] = ['a', 'b', 'c']

ジェネリクスの意味は、このようにしてオブジェクトが持っている属性、オブジェクトの中身についても、型ヒントが書けるようになります。

ジェネリクスの書き方は、関数をイメージするといいかなと思います。型ヒントを引数に取る型ヒントのよう感じです。

例 ユーザ定義クラス

◯ before

これをジェネリクスで表現します。

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

vector0 = Vector(0,   1  )
vector1 = Vector(0.0, 0.1)

◯ after

Generic クラスを継承したクラスを作成します。 与えたい引数の個数だけ TypeVar クラスのインスタンス化して、Generic クラスの引数に加えます(添字表記の中に加えます)。

from typing import Generic, TypeVar 


T = TypeVar('T')

class Vector(Generic[T]):
    def __init__(self, x: T, y: T):
        self.x, self.y = x, y


# 中身は int の Vector
vector0: Vector[int]   = Vector(0, 1)

# 中身は float の Vector
vector1: Vector[float] = Vector(0.0, 0.1)

# 中身は int の Vector と宣言して
# float を使うとエラーになる。
vector2: Vector[int]   = Vector(0, 0.1)

# 中身は float の Vector と宣言して
# int を使ってもエラーに **ならない** 。
vector3: Vector[float] = Vector(0.0, 1)

◯ エラーについて

1つだけエラーで弾かれます。

$ mypy sample.py
sample.py:19: error: Argument 2 to "Vector" has incompatible type "float"; expected "int"
Found 1 error in 1 file (checked 1 source file)
$
# これがエラーにならないのは...
vector3: Vector[float] = Vector(0.0, 1)

# これがエラーにならないことから
# 雰囲気が伝わればと...
z: float = 2

In Python, certain types are compatible even though they aren’t subclasses of each other. For example, int objects are valid whenever float objects are expected.
Duck type compatibility - mypy

◯ TypeVar について

このようにして名前を与えるのはデバックの際に表示したいからということなのでしょうか。

T = TypeVar('T')
assert T.__name__ == 'T'

クラス定義文や関数定義文では自動的に str 型の名前が付与されます。

class C:
    pass

assert C.__name__ == 'C'

def f():
    pass

assert f.__name__ == 'f'

文字列を与えているのは、名前を与えたいからだと思われます。

T = TypeVar('T')
assert T.__name__ == 'T'

D = type('D', (), {})
assert D.__name__ == 'D'

g = lambda x: x**2
g.__name__ = 'g'
assert g.__name__ == 'g'

なぜ名前がわざわざ付与されているのでしょうか? PEP 8 で lambda が使用できない理由を思い出してください。

短くも書ける。

ツールが 、型推論してくれるなら、このように短くも書けます。

vector0 = Vector[int](0, 1)
vector1 = Vector[float](0.0, 0.1)
vector2 = Vector[int](0, 0.1)  # これはエラーになる。
vector3 = Vector[float](0.0, 1)

mypy なら型推論してくれます。

Mypy considers the initial assignment as the definition of a variable. If you do not explicitly specify the type of the variable, mypy infers the type based on the static type of the value expression:
Type inference and type annotations - mypy