Python3.10で追加された型ヒント関連機能から、 PEP 647 User-Defined Type Guards を解説します。
型の絞り込みと型ガード¶
mypyなどの静的な型チェッカが型を推論する時、プログラムの処理を参考にして、可能な型の種類を絞り込んでいます。
例えば、
var1: int | None = func1()
print(var1 + 1) # 型チェックエラー
というプログラムでは、変数 var1 の型は、 int または None のどちらかです。var1 が int の場合は var1 + 1 を正常に計算できますが、 None の場合には None + 1 という計算になってしまうため、型チェッカはこの行をエラーとします。
しかし、このプログラムを修正して、次のように isinstance() を使って明示的に型チェックを行う処理を追加すると、None + 1 という計算が行われる可能性がなくなるため、エラーとはなりません。
var1: int | None = func1()
if isinstance(var1, int):
print(var1 + 1)
このように、プログラムの処理から型の情報を取得することを、型の絞り込み(type narrowing) といいます。また、isinstance() のように、型チェッカが型の絞り込みに利用する条件のことを、型ガード(type guard) といいます。
型チェッカは、isinstance() の他にも、次のような比較条件なども型ガードとして利用します。次の例では、var1 の値は 0 か 1 のどちらかですが、var2 の値はかならず 0 となります。型チェッカはこの情報を参照して、var2 に 0 を代入しようとしていることを検出し、この行をエラーとします。
from typing import Literal
def func(var1: Literal[0, 1])->None:
# var1 は 0 又は 1
if var1 == 1:
# var1 は必ず 1
return
else:
# var1 は必ず 0
var2: Literal[1] = var1 # 型エラー
絞り込みの問題点¶
型チェッカはこのように型の絞り込みを行い、データを推論していろいろなエラーを検出してくれます。しかし、どんな条件でも型ガードとして利用できるわけではなく、人間から見れば明らかでも、型チェッカにはうまく推論できない場合があります。
たとえば、次の例を考えてみましょう。
from typing import Any, TypedDict
class UserInfo(TypedDict):
name:str
def print_user(user:UserInfo)->None:
print("Username:", user['name'])
def foo(d: dict[Any, Any])->None:
if 'name' in d:
if isinstance(d['name'], str):
print_user(d) # 型エラー
foo({'name': 'user name'})
UserInfo は、{'name': 'user name'} のように name というキーの文字列要素を持つ辞書オブジェクトです。上の例では
if 'name' in d:
if isinstance(d['name'], str):
print_user(d) # 型エラー
のように辞書の要素をチェックして、dがUserInfo型に適合することを確認してから print_user(user:UserInfo) に渡しています。
このプログラムは、実行すればなんの問題もなく動きますが、型チェッカはこの処理を型ガードとして認識できず、d を UserInfo型に絞り込むことができません。
このため、print_user(d) の行は型エラーとなってしまいます。
エラーを解消するには、
print_user(d)
を、明示的に型を指定して
print_user(cast(UserInfo, d))
と書き直す必要があります。
ユーザ定義型ガード¶
このように複雑な条件から絞り込みを行うために、PEP 647 User-Defined Type Guards でユーザが独自の型ガードを、関数として定義できるようになりました。
ユーザ定義型ガードは、次のように定義します。
from typing import TypeGuard
def is_userinfo(v: dict[Any, Any])->TypeGuard[UserInfo]:
if 'name' in d:
if isinstance(d['name'], str):
return True
return False
ユーザ定義型ガードは戻り値を typing.TypeGuard[型名] とする関数で、関数が True を返すと、関数の第一引数(ここでは v) が TypeGuardに指定した型(ここではUserInfo) に絞り込まれます。ユーザ定義型ガードを使うと、このプログラムを次のように書き直せます。
from typing import Any, TypedDict, TypeGuard
class UserInfo(TypedDict):
name:str
def print_user(user:UserInfo)->None:
print("Username:", user['name'])
def is_userinfo(v: dict[Any, Any])->TypeGuard[UserInfo]:
if 'name' in d:
if isinstance(d['name'], str):
return True
return False
def foo(d: dict[Any, Any])->None:
if is_userinfo(d):
# d は UserInfo
print_user(d) # 型エラーとはならない
foo({'name': 'user name'})