Last Updated: 2/6/2024, 5:44:57 AM

# Union と Optional ってなに?

最近の Python では型を明示できるようになりました。 ところで変数に未定義の None が入る可能性が場合には、 どうやって型を明示すればいいのでしょうか?

# sample.py
a: int
a = None  # mypy では error になる
$ mypy sample.py
sample.py:2: error: Incompatible types in assignment
(expression has type "None", variable has type "int")
$

# 1. Union 型

Union を使うことによって、 2つの型が代入される可能性があることを明示できます。

from typing import Union
b: Union[int, None]
b = None
$ # エラーにならない
$ mypy sample.py
$

typing.Union (opens new window)
ユニオン型; Union[X, Y] は X または Y を表します。

# 2. Optional 型

None を返してくるかもしれない関数は良くあります。 そのため Optional という専用の型があります。

from typing import Optional
c: Optional[int]
c = None
$ # エラーにならない
$ mypy sample.py
$

typing.Optional (opens new window)
Optional[X] は Union[X, None] と同値です。

# 3. cast

ある int 型を引数にとる関数に Optinal[int] 型の変数を渡すことはできません。

from typing import Optional


a: Optional[int] = 0

def add_one(x: int) -> int:
    return x + 1

print(add_one(a))
$ mypy
sample.py:9: error:
  Argument 1 to "add_one" has
  incompatible type "Optional[int]";
  expected "int"
Found 1 error in 1 file (checked 1 source file)
$

Optinal[int] を int に変換 cast してあげる必要があります。

from typing import Optional, cast


a: Optional[int] = 0

def add_one(x: int) -> int:
    return x + 1

a = cast(int, a)

add_one(a)
$ mypy sample.py 
Success: no issues found in 1 source file
$

typing.cast(typ, val) (opens new window)
値をある型にキャストします。 この関数は値を変更せずに返します。 型検査器に対して、返り値が指定された型を持っていることを通知しますが、実行時には意図的に何も検査しません。 (その理由は、処理をできる限り速くしたかったためです。)

# 4. typing.re

標準ライブラリ re でハマりました。 直接 re から import したものを、型に使うと mypy にエラーで弾かれます。 型をつける時だけ typing.re から import してください。

from typing import re  # <--- OK
# import re            # <--- NG

なぜ、このようなことになるのかは理解できていません... orz

# ◯ 他にも落とし穴はあるのか?

typing.Hashable, typing.Sized, typing.Text, typing.Pattern, typing.Match が該当しそうな気配があります。

他にも似たような落とし穴があるのか気になりました。 ドキュメントを確認したところ alias と言う文言が見えたので、この ailias と言う文言がある型だけ、 とりあえず気をつけておけば大丈夫なのかなと思っています。

class typing.Match (opens new window)
class typing.Patter (opens new window)
These type aliases correspond to the return types from re.compile() and re.match(). These types (and the corresponding functions) are generic in AnyStr and can be made specific by writing Pattern[str], Pattern[bytes], Match[str], or Match[bytes].

# 5. 具体例

re.search 関数の返り値の型は Optional[Match] です。 re.search を例にとり Optional, cast, typing.re を振り返っていきます。 re.search 関数の挙動は、以下のような具合です。

#
# 対話モード >>> に
# コピペで実行できます。
#
import re

# (a) あれば Match 型のオブジェクトを返してくれるけど...
result = re.search('<(\d+)>.+"(\w+)"', '1v2<4649>90a"abcd"1a')
assert result.groups() == ('4649', 'abcd')
assert isinstance(result, re.Match)

# (b) ないときは type(None) 型のオブジェクトを返す。
result = re.search('<(\d+)>.+"(\w+)"', 'abcdefg')
assert isinstance(result, type(None))

# 5.1. そのままで...

groups 関数を定義しました。 この関数は re.Match クラスのオブジェクトしか、引数として受け付けません。

import re

# re.Match クラスのオブジェクトしか受け付けない関数
def groups(result):
    return result.groups()

result_1 = re.search(r'<(\d+)>.+"(\w+)"', '1v2<4649>90a"abcd"1a')
print(groups(result_1))

result_2 = re.search(r'<(\d+)>.+"(\w+)"', 'abcdefg')
print(groups(result_2))  # <- コピペで実行するとエラーになります。
>>> print(groups(result_2))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in groups
AttributeError: 'NoneType' object has no attribute 'groups'
>>> 

変数 result_2 には None がはいっていたためエラーを投げ返されました。  実行した後に  型に関する間違いを見つけることができました。

# 5.2. 型ヒントで...

型アノテーションを使えば、実行する前に気づくことができます。 上記のコードを、型アノテーションをつけると次のようになります。

import re
from typing import Match  # <- 本当に落とし穴
from typing import Optional
from typing import Sequence
from typing import cast


# re.Match クラスのオブジェクトしか
# 受け付けない関数
def groups(result: Match[str]) -> Sequence[str]:
    return result.groups()


# 1)
result_1: Optional[Match[str]]
result_1 = re.search(r'<(\d+)>.+"(\w+)"', '1v2<4649>90a"abcd"1a')
# このケースでは None が返されないことはわかっているので、
# groups 関数で受けられるようにキャストする。
cast_result_1 = cast(Match[str], result_1)
print(groups(cast_result_1))

# 2)
result_2: Optional[Match[str]]
result_2 = re.search(r'<(\d+)>.+"(\w+)"', 'abcdefg')
# groups 関数で受けられるようにキャストはしない(None が返されるので)。
# cast_result_2 = cast(Match[str], result_2)
print(groups(result_2))

これを mypy で型検査すると、次のように怒られます。  実行する前に  型に関する間違いを見つけることができました。

$ mypy sample.py 
sample.py:22: error: Argument 1 to "groups" has incompatible type "Optional[Match[Any]]"; expected "Match[str]"
$

# 6. 型ヒントのメリットとデメリット

型ヒントを使う以外にも以下二点のような、例外的な状況を伝える方法を見ていきました。

型ヒントのメリットは、実際に実行せずに、型に関するエラーだけは、見つけることができます。 上記の例では分かりにくいですが。 None を return するか、あるいは例外を raise する方法だと、 実際に実行させて、バグを見つけるスタイルになります。 これは見落としが生じる可能性があります。

型ヒントのデメリットは、面倒であることです。 例外であれば、そのまま呼び出し元の関数に例外が順次 raise されていきます。 cast したり return しないといけません。 また、エラーが判別できる範囲も型だけに限定されています。

# 7. まとめ

ここまで以下のように見てきました。

以上になります。ありがとうございました。