Python3.10の新機能 Python 3.10の新機能(その7) ユーザ定義型ガード

(その1) パターンマッチ(その2) with文のネスト(その3) エラーメッセージの改善(その4) | 演算子によるユニオン型の指定(その5) パラメータ仕様変数(その6) 明示的な型エイリアス(その7) ユーザ定義型ガード(その8) OpenSSL 1.1.1が必須に(その9) zip()関数に引数 strict を追加(その10) Dataclassでslotsが利用可能に(その11) その他の変更

型の絞り込みと型ガード

mypyなどの静的な型チェッカが型を推論する時、プログラムの処理を参考にして、可能な型の種類を絞り込んでいます。

例えば、

var1: int | None  = func1()
print(var1 + 1)  # 型チェックエラー

というプログラムでは、変数 var1 の型は、 int または None のどちらかです。var1int の場合は 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 のどちらかですが、var2 の値はかならず 0 となります。型チェッカはこの情報を参照して、var20 を代入しようとしていることを検出し、この行をエラーとします。

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)  # 型エラー

のように辞書の要素をチェックして、dUserInfo型に適合することを確認してから print_user(user:UserInfo) に渡しています。

このプログラムは、実行すればなんの問題もなく動きますが、型チェッカはこの処理を型ガードとして認識できず、dUserInfo型に絞り込むことができません。 このため、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'})
Copyright © 2001-2023 python.jp Privacy Policy python_japan
Amazon.co.jpアソシエイト
Amazonで他のPython書籍を検索