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'})