型ヒント

編集

型ヒント(Type Hints)は、Python 3.5以降で導入された機能で、変数や関数の引数、返り値などに対して、その期待される型情報を注釈として記述することができます。型ヒントは静的型付けの一形態であり、コードの可読性を高めたり、静的解析ツールによるチェックを容易にしたりするのに役立ちます。

型ヒントは通常、次のようにして使います:

def greet(name: str) -> str:
    return f"Hello, {name}"

上記の例では、greetという関数の引数nameの型をstr(文字列)として指定し、返り値の型をstrとして指定しています。

一般的な型ヒントの使用法:

  • 基本的な型: int, float, str, boolなどの基本的なPythonの組み込み型が使えます。
  • コンテナ型: List, Tuple, Dict, Setなどのコンテナ型に対する型情報も指定できます。例えば、List[int]は整数のリストを表します。
  • ユーザー定義型: クラスやタイプエイリアス(typingモジュールを使って定義される型)など、ユーザーが定義した型も指定できます。

型ヒントは実行時の振る舞いには影響を与えませんが、IDEや静的解析ツール(mypyなど)などのツールによって型のチェックが行えます。これにより、バグの早期発見やコードの保守性の向上が期待できます。

ただし、Pythonは動的型付け言語であり、厳密な静的型付け言語ではないため、型ヒントはあくまで参考情報として扱われることに注意してください。

型アノテーション

編集

型アノテーション(Type Annotation)は、Pythonのコードで変数、関数の引数や返り値、クラスの属性などに対して型情報を記述する方法です。これは、静的型チェッカーやIDEのための情報提供や、コードの可読性向上に役立ちます。

Pythonでは型アノテーションは以下のように使います:

  1. 変数への型アノテーション:
    x: int = 5
    
    ここでの: intが型アノテーションで、変数xの型を整数(int)であると示しています。
  2. 関数の引数と返り値への型アノテーション:
    def add(a: int, b: int) -> int:
        return a + b
    
    add関数の引数ab、そして返り値に対してそれぞれの型を示しています。引数の型アノテーションは引数名の後ろに:を使い、返り値の型アノテーションは関数の最後の->の後ろに指定します。
  3. クラスの属性への型アノテーション:
    class Person:
        def __init__(self, name: str, age: int) -> None:
            self.name = name  # name属性はstr型
            self.age = age    # age属性はint型
    
    Personクラスの__init__メソッド内で、name属性とage属性の型をそれぞれstrintとして指定しています。

型アノテーションはPythonの実行時には影響を与えず、実行時の型チェックを行わないため、静的型付け言語のように完全な型安全性を提供するものではありません。しかし、IDEや静的解析ツール(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__属性を使って型チェックを行うためには、外部ライブラリを使用する必要があります。

参考文献

編集