今朝、PEP 647 (User-Defined Type Guards) が Accepted になったというPRを見かけました。
そこで、今回は PEP 647 を読んでみようと思います。
概要
-
型チェッカーツールでは type narrowing と呼ばれる手法を使って、プログラム内で型情報をより正確に決定しています。
以下の例では if文と
is None
を利用して、自動的に if 文の中の型が絞り込まれます。def func(val: Optional[str]): # "is None" type guard if val is not None: # Type of val is narrowed to str ... else: # Type of val is narrowed to None ...
他にも
isinstance()
など、いくつかの判定で type narrowing が行われています。 -
しかし、以下のようにユーザー関数を利用して判定している場合には、type narrowing は意図通りには働きません。
def is_str_list(val: List[object]) -> bool: """Determines whether all objects in the list are strings""" return all(isinstance(x, str) for x in val) def func1(val: List[object]): if is_str_list(val): print(" ".join(val)) # Error: invalid type
-
そこで、新しい型情報
typing.TypeGuard
を通じて ユーザー定義 型ガード (user-defined type guard) を定義できるようにします -
ユーザー定義 型ガードを用いることで、type narrowing のサポートを受けやすくなります
-
ユーザー定義 型ガードは pyright にはすでに実装済みで、
typing.TypeGuard
による定義は 3.10 から利用可能です
アプローチ
is_str_list()
のような bool を返す関数の返り値の型として typing.TypeGuard
を指定します。
from typing import TypeGuard
def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
"""Determines whether all objects in the list are strings"""
return all(isinstance(x, str) for x in val)
型チェッカーは、この関数が True を返した場合に、先頭の引数を TypeGuard
に指定した型に合致するとみなします。上記の例では、 is_str_list()
をパスしたデータは List[str]
として扱われます。
なお、この関数が False を返した場合は type narrowing は行われません。
以下の例では、 if is_two_element_tuple(...)
のブロックでは type narrowing が行われた結果、型が Tuple[str, str]
に絞り込まれるのに対して、else ブロックでは型は変化しません。
def is_two_element_tuple(val: Tuple[str, ...]) -> TypeGuard[Tuple[str, str]]:
return len(val) == 2
OneOrTwoStrs = Union[Tuple[str], Tuple[str, str]]
def func(val: OneOrTwoStrs):
if is_two_element_tuple(val):
reveal_type(val) # Tuple[str, str]
...
else:
reveal_type(val) # OneOrTwoStrs
Union を使っているので、 Tuple[str]
に絞り込まれるような印象を持ちますが、ユーザー定義 型ガードではそのようには動かないので注意が必要です。
感想
- 条件文で型が絞り込まれていることは知っていたけど、type narrowing と呼ばれているのは知らなかった
- 致し方なく
type: ignore
している箇所が減らせるのは気持ちよさそう - 細かすぎる機能な気がするものの、かゆいところに手が届く感じがある