Python/型ヒント

出典: フリー教科書『ウィキブックス(Wikibooks)』

型ヒント[編集]

Python は、動的型付けの言語で変数・関数の仮引数や関数の戻り値には型を指定する必要がありません(指定する方法が、従来は存在しませんでした)。

この性質は、普遍的なアルゴリズムを書くなどの用途に適していますが、たとえば、リストの値の合計を求めるつもりで書いた関数が、文字列を要素とするリストに適用さるとリストの要素を連結した文字列を返すなど、コードを書いたときの想定とは異なる使われ方をしたとき、劇的に違う実行結果を招いてしまうことがあります。

型アノテーション[編集]

Pythonにおいて、型アノテーション(Type Annotation)は変数、仮引数、戻り値などに対する型の注釈であり、Python 3.5から正式にサポートされています。型アノテーションはPythonの構文解析に含まれますが、Pythonのインタープリターによる型の検査は行われず、静的型検査を行う外部プログラム(MyPyなど)によって実行されます。これにより、コードの可読性と保守性が向上し、バグの発見や修正が容易になります。


型アノテーションのない従前のコード[編集]

合計のはずが連結に
def total(*args):
    if len(args) == 0:
        return 0
    it = iter(args)
    result = next(it)
    for i in it:
        result += i
    return result

print(f"""\
{total()=}
{total(1,2,3)=}
{total(*(i for i in range(10)))=}
{total(*(1.0*i for i in range(10)))=}
{total(False, True)=}
{total("abc","def","ghi")=}
{total([0,1,2],[3,4,5],[6,7,8])=}""")
実行結果
total()=0
total(1,2,3)=6
total(*(i for i in range(10)))=45
total(*(1.0*i for i in range(10)))=45.0
total(False, True)=1
total("abc","def","ghi")='abcdefghi'
total([0,1,2],[3,4,5],[6,7,8])=[0, 1, 2, 3, 4, 5, 6, 7, 8]

型アノテーションのあるコード[編集]

合計のはずが連結に(オンライン実行)
合計のはずが連結に(mypyで検査)
from typing import Union
number = Union[int, float]

def total(*args: number) -> number:
    if len(args) == 0:
        return 0
    it = iter(args)
    result = next(it)
    for i in it:
        result += i
    return result

print(f"""\
{total()=}
{total(1,2,3)=}
{total(*(i for i in range(10)))=}
{total(*(1.0*i for i in range(10)))=}
{total(False, True)=}
{total("abc","def","ghi")=}
{total([0,1,2],[3,4,5],[6,7,8])=}""")
実行結果
上に同じ
MyPyの検査結果
main.py:13: error: Argument 1 to "total" has incompatible type "str"; expected "Union[int, float]"
main.py:13: error: Argument 1 to "total" has incompatible type "List[int]"; expected "Union[int, float]"
main.py:13: error: Argument 2 to "total" has incompatible type "str"; expected "Union[int, float]"
main.py:13: error: Argument 2 to "total" has incompatible type "List[int]"; expected "Union[int, float]"
main.py:13: error: Argument 3 to "total" has incompatible type "str"; expected "Union[int, float]"
main.py:13: error: Argument 3 to "total" has incompatible type "List[int]"; expected "Union[int, float]"
Found 6 errors in 1 file (checked 1 source file)
型アノテーションを追加しても実行結果は同じですが、MyPyは6つの型の不整合を発見しています。
from typing import Union
number = Union[int, float, None]
は、python 3.9 以降では
number = int | float | None
と、typingモジュールを頼らず書けますが、3.9より前のバージョンでは Syntax error になるので、互換性を考えると
try:
    number = int | float | None
except TypeError as e:
    """ Fallback for < 3.9 """
    from typing import List
    number = Union[int, float, None]
の様に実行環境によって動作を変える様にできます。
が、ここ書き方だと MyPy が2行目と6行目で型定義が重複していると警告します。
python3.9以降でも typingモジュールは有効なので
from typing import Union
number = Union[int, float, None]
のままにしました。

型ヒント・型アノテーションは、まだ新しい機能で typing モジュールを import しない方向に進化しつつあり、今後の動向が注目されます。

__annotations__ 属性[編集]

オーブジェクトの型アノテーションについては、__annotations__ 属性によって Python プログラム中からアクセスできます。

__annotations__ 属性
import sys
print(f"{sys.version=}")

v: int
v = 0

print(f"{__annotations__=}")

def f(a: int, b: int) -> int:
    _anon = f.__annotations__
    assert isinstance(a,_anon["a"])
    assert isinstance(b,_anon["b"])
    print(f"{f.__annotations__=}")
    result = a + b
    assert isinstance(result,_anon["return"])
    return result

class Point(dict):
    x: int
    y: int
    x = 10
    y = 20
    print(f"{__qualname__}: {__annotations__=}")

try:
    f(2,3)
    f("","")
except AssertionError as err:
    print(f"{err=}")
print("done!")
実行結果
sys.version='3.8.10 (default, Jun  2 2021, 10:49:15) \n[GCC 9.4.0]'
__annotations__={'v': <class 'int'>}
Point: __annotations__={'x': <class 'int'>, 'y': <class 'int'>}
f.__annotations__={'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
err=AssertionError()
done!
型アノテーションは、名前の通り原則的には名前通り注釈なのですが、特殊属性 __annotations__ を介して実行中のスクリプト自身からアクセスできます。
これを利用すると、例えば関数が実引数が仮引数の型アノテーションと一致しているかをisinstance関数で確認できます。
isinstance関数で確認し、不一致があった場合 assert 文で AssertError をあげ、try:except で捕捉することができます。

このように、__annotations__属性を使用することで、コードの可読性や保守性を向上させることができます。ただし、__annotations__属性はPythonのランタイムには影響を与えないため、必ずしも正確である必要はありません。また、__annotations__属性を使って型チェックを行うためには、外部ライブラリを使用する必要があります。

参考文献[編集]