Python 3.11でも、型ヒント 関連で多くの機能が追加されました。まず、長年望まれていた新機能、 PEP-646 可変長ジェネリックス の概要を紹介します。
可変長ジェネリックスについて説明する前に、まず普通のジェネリックスを復習しましょう。
ジェネリックス¶
まず、引数 a を受け取り、要素が a だけのタプルを返す関数を考えてみましょう。
def to_tuple(a):
return (a,)
to_tuple('100') を実行すると、結果としてタプル ('100',) が返ります。
この関数に型ヒントを指定してみましょう。引数 a はどんな型でも構いませんから、一番簡単なのは typing.Any を使って次のように書く方法でしょう。
from typing import Any
def to_tuple(a: Any) -> tuple[Any]:
return (a,)
value = to_tuple(100) # value の型は tuple[Any]
この場合、to_tuple(100) の結果を代入した value の型は tuple[Any] となります。しかし、引数に明示的に int 型のオブジェクト 100 を指定しているのに、結果のタプルからはその情報が失われて Any になってしまうのはもったいないですね。
型変数¶
こういった場合、Pythonでは変数の型を受け取る変数 型変数(TypeVar) を使って、具体的な型の名前を指定せず、汎用的な型定義を指定できるようになっています。
from typing import TypeVar
T = TypeVar("T")
def generic_to_tuple(a: T) -> tuple[T]:
return (a,)
value = generic_to_tuple(100) # value の型は tuple[int]
to_tuple() の引数には a: Any と書いて、 a の型は Any であることを宣言しました。一方、generic_to_tuple() では、TypeVar で作成した型変数 T を利用し、a: T と宣言しています。
このように記述すると、generic_to_tuple(a: Any) のように明示的に a の型を指定するのではなく、a の型を型変数 T に代入する、という指定になります。この例では generic_to_tuple(100) と引数 a には int 型のオブジェクトを指定していますから、型変数 T は int となります。
generic_to_tuple() の戻り値は、型変数 T を使って tuple[T] と指定しています。これにより、引数 a が int なら戻り値は tuple[int]、 引数 a が文字列型なら tuple[str] になります。
to_tuple() では戻り値を tuple[Any] のように明示的に Any を指定しているので、どんな引数を指定しても、戻り値は必ず tuple[Any] になってしまいます。しかし、generic_to_tuple() のように型変数を使えば、引数に応じて変化する戻り値の型を指定できます。
このように型変数を使った型ヒントは ジェネリックス と呼ばれます。ジェネリックスを使うと、事前に決まった特定の型を明示的に記述するのではなく、入力に応じて変化する汎用的な型ヒントを指定できます。
可変長ジェネリックス¶
もう一つの例として、任意のタプルを受け取り、そのタプルの末尾に 0 を付加したタプルを返す関数を考えてみましょう。
def append_zero(a: tuple[Any, ...]) -> tuple[Any, ...]:
return (*a, 0)
value = append_zero((1, 'a')) # valueの型は tuple[Any, ...]
この場合、引数として(1, 'a') を指定していますが、戻り値は tuple[Any, ...] になってしまいます。引数の値は tuple[int, str] と型が明示的に明らかなのに、戻り値に反映できないのはもったいないですね。 この戻り値の型を tuple[int, str, int] にする方法はないでしょうか?
従来の TypeVar は型を一つしか代入できないため、 tuple[int, str] のようにタプル型を代入することはできますが、タプルの要素それぞれの型(ここでは int と str)のような、複数の値の型を代入することはできません。
そこで、Python3.11では、複数の型を代入する 型変数タプル TypeVarTuple が追加されました。TypeVarTuple を使って、append_zero() を書き直してみましょう。
from typing import TypeVarTuple
Ts = TypeVarTuple("Ts")
def variadic_append_zero(a: tuple[*Ts]) -> tuple[*Ts, int]:
return a + (0,)
value = variadic_append_zero((1, 'a')) # valueの型は tuple[int, str, int]
TypeVarTuple を使った型変数タプル Tsは、tuple[*Ts] のように、* をつけて使用します。この例では、Ts には a に指定したタプルのすべての要素の型が代入されます。この例では、引数 a にタプル (1, 'a') が指定されていますから、Ts にはタプルの2つの要素の型である int と str が代入されます。
TypeVarTupleのアンパック¶
variadic_append_zero() の戻り値は tuple[*Ts, int] となっています。これは、戻り値のタプルの要素の型は、引数 a のすべての要素の型に int を追加したもの、という意味になります。引数 a が (1, 'a') の場合、a の型は tuple[int, str] ですので、tuple[*Ts, int] の結果は tuple[int, str, int] となります。
variadic_append_zero() の戻り値 [*Ts, int] という記述は、次のような普通のタプルのアンパック
>>> tp = (1, 2, 3)
>>> print((*tp, 'A'))
(1, 2, 3, 'A')
に似ていますね。 TypeVar が型を代入する変数だとすると、TypeVarTuple は複数の型を登録できる、型のタプルに相当します。
引数の型指定にも、同じ形式で記述できます。次の関数 variadic_get_middle() は、引数として先頭が整数、末尾が文字列のタプルをとり、先頭と末尾の要素を削除したタプルが戻り値となります。
from typing import TypeVarTuple
Ts = TypeVarTuple("Ts")
def variadic_get_middle(a: tuple[int, *Ts, str]) -> tuple[int, *Ts, int]:
return (0,) + a[1:-1] + (1,)
value = variadic_get_middle((0, 1, '2', 'a')) # valueの型は tuple[int, str, str]
reveal_type(value) # value は tuple[builtins.int, builtins.int, builtins.str, builtins.int]
value2 = variadic_get_middle(("X", 1, '2', 'a')) # 先頭が整数ではないのでエラー
ここでは、引数 a の型を tuple[int, *Ts, str] と指定しています。これは、最初の要素が int で、0個以上の複数の要素が続き、最後の要素が str であるようなタプルに一致します。先頭と末尾以外の要素の型は、型変数タプル Ts に代入されます。引数 a が (0, 1, '2', 'a') の場合、Ts には 1 の型である int と '2' の型である str が代入されます。
この書き方は、通常のタプルのアンパック代入を思い出すとわかりやすいのではないでしょうか? 例えば、タプル (1, 2, 3, 4) は、次のように複数の変数に分割して代入できます。
>>> (a, *b, c) = (1, 2, 3, 4)
>>> print(a)
1
>>> print(b)
[2, 3]
>>> print(c)
4
この場合、変数 a には先頭の 1 が代入され、c には末尾の 4 が代入されます。b には中間の 2 と 3 がリストとして代入され、値は [2, 3] となります。代入文の (a, *b, c) と、variadic_get_middle() で指定した型ヒント tuple[int, *Ts, str] を比べてみてください。
可変長引数¶
TypeVarTuple は、関数の可変長位置変数にも利用できます。次の関数 len_args() は、指定した引数の数と、すべての引数のタプルを返します。
from typing import TypeVarTuple
Ts = TypeVarTuple("Ts")
def len_args(*args: *Ts) -> tuple[int, *Ts]:
return (len(args),) + args
tp = len_args(1, '2', 3.0)
この例では、tp の値は (3, 1, '2', 3.0) となり、型は tuple[int, int, str, float] となります。
また、可変長位置変数には tuple を使ったアンパックも指定できます。次の例では、最後の引数が str でなければエラーとなります。
from typing import TypeVarTuple
Ts = TypeVarTuple("Ts")
def followed_by_str(*args: *tuple[*Ts, str]) -> None:
assert isinstance(args[-1], str)
print(args)
followed_by_str(1, 2, "3") # Ok
followed_by_str(1, 2, 3) # エラー