ジェネリックなクラスと関数がシンプルな型引数構文で作成可能に
Python 3.12では、ジェネリックなクラスや関数の定義が多くの型付け言語(Java、TypeScriptなど)と同様にシンプルな構文で記述できるようになりました。
TypeVarやGenericを用いたジェネリクス
Python 3.12より前のジェネリクスについては、本連載第2回の「ジェネリクスとは?」で軽く紹介しました。以下のリストは、Python 3.11でジェネリクスな関数とクラスを定義する例です。
from typing import TypeVar, Generic (1) T = TypeVar("T") (2) def function(x: T, y: T) -> T: (3) return x class MyClass(Generic[T]): (4) def method(self) -> T: ...
(1)のように、TypeVarやGenericといったクラスをインポートする必要があります。
(2)のように、実際の型を格納する型変数Tをグローバルに宣言する必要があります。
(3)ではジェネリックな関数を定義していますが、このとき型変数Tが型ヒントに使われます。
(4)ではジェネリックなクラスを定義していますが、このときGenericクラスの継承とともに型変数Tが型ヒントに使われます。
ここから、開発者の間では以下のような点が指摘されてきたようです。
- typingモジュールからTypeVar、Genericなどのインポートが必要
- 型変数の宣言が必要
- 型変数がグローバルに宣言されることでのコンテキストの問題
極めて基本的な機能にもかかわらず、言語仕様に組み込まれていないため、モジュールのインポートが常に必要です。また、ジェネリック型を型変数として宣言する必要があります(変数名とTypeVarの引数が同一でなければならない点も冗長といえます)。さらに、関数定義とクラス定義で同一の型変数を使っていますが、これは本来は別のものであるべきで、その場合には型変数を区別できる形でいくつも宣言する必要があります。
新しい型引数構文
このような背景を受けて、Python 3.12では非常にシンプルな型引数構文でジェネリックな関数やクラスを定義できるようになりました。本連載でこれまで紹介してきたTypeVarなどによる型変数の宣言や、継承元のGenericクラスが必要なく、シンプルに記述できます。以下の2つのリストは、関数の定義を型引数構文の利用の有無で比較しています(import文などの違いを明確にするために別ファイルにしています)。
from typing import TypeVar T = TypeVar("T") def function(x: T, y: T) -> T: return x
def function[T](x: T, y: T) -> T: return x
TypeVarのimport文と型変数の宣言が不要になるので、非常にシンプルになります。クラスも同様です。以下の2つのリストは、クラスの定義を型引数構文の利用の有無で比較しています(import文などの違いを明確にするために別ファイルにしています)。
from typing import TypeVar, Generic, Iterator T = TypeVar("T") class MyClass(Generic[T]): def method1(self) -> T: ...
class MyClass[T]: def method(self) -> T: ...
記述がシンプルになることの他に、指定した型引数はそのコンテキスト内でしか有効でなくなるので、型変数の名前の衝突などを気にする必要がなくなります。
TypedDictの使用で、**kwargsの型付けをより厳密に
関数の引数**kwargs(可変長のキーワード引数)に対する型ヒントは、全てのキーワード引数が同じ型でなければならないという制約がありました。例えば以下のリストでは、関数の引数に型ヒントstrを指定しているので、全てのキーワード引数の型がstrであると解釈され、型が異なる引数があれば型チェックでエラーとなります。逆に、関数が想定していない引数が与えられてもエラーにはなりません。
def function(**kwargs: str): # すべてのキーワード引数はstr for key, value in kwargs.items(): print(f"{key}: {value}") function(name='Nao', birth=1980) # birthに整数リテラルを割り当てることはできない function(name='Nao', gender='m') # genderを引数に与えてもエラーにならない
このような制約で型ヒントの恩恵も受けづらいということで、Python 3.12ではTypedDict(型付き辞書)を使って引数ごとの型ヒントの指定が可能になりました。以下のリストのように、要素と型を列挙した辞書を作成し、**kwargsの型ヒントにUnpackして指定するだけです。辞書にある引数は全て与える必要があり、辞書にない引数を与えると、型チェックでエラーになります。
from typing import TypedDict, Unpack class KWArgs(TypedDict): name: str birth: int def function(**kwargs: Unpack[KWArgs]): for key, value in kwargs.items(): print(f"{key}: {value}") function(name='Nao', birth=1980) # 受け入れられる function(name='Nao', birth='1980') # birthがintでないのでエラーとなる function(name='Nao') # birthがないのでエラーとなる function(name='Yamauchi', birth=1980, gender='m') # genderは指定できない
typing.overrideの導入で、オーバライドの意図がより明確に
スーパークラスのメソッドをオーバライドする際に、メソッド名を含むシグネチャが間違ったりすることがあります。オーバライドなのか、それとも別のメソッドなのかは型チェックツールには分からないので、バグの要因の一つとなっています。Javaなどの言語ではアノテーションを指定してオーバライドするメソッド定義であることを明示しますが、Python 3.12でデコレータによりそれが可能になりました。
以下のリストでは、親クラスで定義されているget_distanceメソッドを子クラスでオーバライドしようとしていますが、スペルミスで正しくオーバライドされません。@overrideデコレータを指定することで、型チェックツールがこれを検出できます。
from typing import override class Parent: def get_distance(self) -> int: return 50000 class Child(Parent): @override def get_destance(self) -> int: # 型チェックでエラー return 100000