Pythonの型ヒント、UnionとTypeVarの違いを理解する

こんにちは。サービスグループの武田です。Pythonの型ヒントあるUnionとTypeVarの違いを確認してみました。
2020.09.25

こんにちは。サービスグループの武田です。

Pythonは動的型付きのプログラミング言語ですが、3.5から型ヒント(Type Hints)が導入されました。

型ヒントは静的型付き言語のように型によるチェックができるようになる一方で、実行時には影響を及ぼしません。つまり型ヒントはアノテーションです。型ヒントのチェックはコンパイラなどが言語として提供されているわけではありません。代わりにmypyを使うのがデファクトのようです。

このエントリでは型ヒントに含まれているUnionTypeVarの違いを確認してみました。

環境

次のような環境で検証をしています。

$ python3 -V
Python 3.8.3

$ mypy -V
mypy 0.782

Union vs TypeVar

Unionは「str or int」のように「いくつかのある型のうちいずれか」にマッチすればOKという型ヒントです。TypeVarはジェネリクスの型変数を定義するための型ヒントです。こう書くと、そもそも比較するような二者ではないのですが、「似たような使い方」ができてしまうため整理しました。

というわけで次のコードを見てください。

main.py

from typing import TypeVar, Union

U = Union[str, int]

T = TypeVar("T", str, int)


def test_union(p: U):
    pass


def test_typevar(p: T):
    pass


test_union(1)  # OK
test_union("union")  # OK
test_union(None)  # NG

test_typevar(2)  # OK
test_typevar("typevar")  # OK
test_typevar(None)  # NG

チェック結果は次のようになります。

$ mypy main.py
main.py:18: error: Argument 1 to "test_union" has incompatible type "None"; expected "Union[str, int]"
main.py:22: error: Value of type variable "T" of "test_typevar" cannot be "None"
Found 2 errors in 1 file (checked 1 source file)

それぞれstr型かint型の引数を取る関数を宣言しており、実際のチェックもそのように動いています。これだけ見ると両者に違いが見られません(エラーメッセージは異なりますが)。はっきりとした違いを確認するため、次のようにコードを変更してみます。

main.py

from typing import TypeVar, Union

U = Union[str, int]

T = TypeVar("T", str, int)


def test_union(p1: U, p2: U):
    pass


def test_typevar(p1: T, p2: T):
    pass


test_union(1, 2)  # OK
test_union("union", "onion")  # OK
test_union(1, "onion")  # OK


test_typevar(1, 2)  # OK
test_typevar("typevar", "foobar")  # OK
test_typevar(1, "foobar")  # NG

チェック結果は次のようになります。

$ mypy main.py
main.py:23: error: Value of type variable "T" of "test_typevar" cannot be "object"
Found 1 error in 1 file (checked 1 source file)

test_unionはエラーになっていません。引数のp1とp2はそれぞれ型ヒントとしてUを指定していますが、それぞれの引数について「str or int」を確認しています。つまりp1とp2の型が同じか異なるのかは関係ないのです。一方のtest_typevarは3つ目がエラーになっています。引数のp1とp2に型ヒントとしてTを指定していますが、先ほどと違い型変数Tが指定されたパラメーターは すべて同じ型 である必要があります *1。エラーが出た関数呼び出しは、型変数Tが指定されているパラメーターに異なる型の値を与えたためエラーになったというわけです。

まとめると、U = Union[str, int]は「str型かint型のいずれかを許可する複合型」を宣言しています。またT = TypeVar("T", str, int)は「str型かint型に制限した型変数T」を宣言しています。

まとめ

Unionは複合型でTypeVarは型変数、がまずは基本です(そもそもTypeVarのベーシックな使い方はT = TypeVar("T"))。「A or Bの型」の実現方法から入ってしまったため、少し混乱しました。Pythonでも型宣言していきましょう!

脚注

  1. TypeVarはデフォルトが不変です。共変、反変、境界の指定もできるため厳密には常に同じ型とは言えませんが、このケースでは同じ型であることが求められます