# 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
ユニオン型; Union[X, Y] は X または Y を表します。

# 2. Optional 型

実は None だけであれば Optional という便利なものがあったりします。

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

typing.Optional
Optional[X] は Union[X, None] と同値です。

# 3. mypy

Optional ってなんだよって話ですが、 例えば Python では標準ライブラリ research メソッドの返り値の型は Optional[Match] です。 検索対象の文字列があれば Match オブジェクトを返してくれますが、 無ければ None を返します。

# 対話モード >>> に直接コピペできます。
# Match もしくは None = re.search(文字列のパターン, 検索対象の文字列)
import re

# (a) あれば re.Match を返してくれるけど
result = re.search('<(\d+)>.+"(\w+)"', '1v2<4649>90a"abcd"1a')

isinstance(result, re.Match)
# True

result.groups()
# ('4649', 'abcd')


# (b) ないときは None を返す。
result = re.search('<(\d+)>.+"(\w+)"', 'abcdefg')

isinstance(result, type(None))
# True

なにが言いたいかというと変数 result には Match クラスのオブジェクトがはいるかもしれないし、 type(None) クラスのオブジェクトがはいるかもしれないということです。

たいていの場合、1つの変数には1つのクラスのオブジェクトしか入りませんが Match.search メソッドは、2つのうちいずれかのクラスのオブジェクトを返します。 これを型アノテーションで表現すると変数 result は次のようになります。

# 対話モード >>> に直接コピペできます。
# from re import Match <- re から import しない, 
#                         mypy で弾かれる http://bit.ly/2Wh2IU3
from typing import re
from typing import Optional

result: Optional[Match]

Python の型アノテーションと mypy については、 以下の記事でご紹介させていただきました。

# 3.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 がはいっていたためエラーを投げ返されました。 実行することによって初めて自分が間違っていたことに気づくことができました。

# 3.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')
# 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]"
$

cast 関数を使っています。 以下のコードは、変数 cast_result_1 には None が、 代入されない Optional ではないことを明示しています。

cast_result_1 = cast(Match[str], result_1)

# 4. まとめ

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 がはいっていたためエラーを投げ返されました。 実行することによって初めて自分が間違っていたことに気づくことができました。

しかし None 気付けたとしても、どこで間違っているかすぐにはわかりません。 デバッグにあたって、苦労することがあります。

例外を raise すれば間違えた箇所には、すぐに気づくことができます。

None も例外を raise することにも問題点があります。 それはなんでしょうか?

実際に実行してみないとわからないということです。 それも None や例外が raise 投げられないことには、 間違っているかどうかわからないということです。

しかし  Optional を使うことで、None が代入されたり、あるいは例外が投げられなくても、 実行する前に型判定を行い誤りを素早く見つけることができます。 

# 5. おわりに

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

UnionOptional についてご紹介せさせていただきました。 型アノテーションを使うとコードが長くなってしまいますが、

Last Updated: 11/11/2019, 11:17:29 PM
x 消す
銀河英雄伝説が無料!