Python | テスト・設計・品質:Union

Python Python
スポンサーリンク

Unionって何?一言でいうと「型の“どれか”を許すための道具」

Union は、型ヒントの世界で
「この値は、A か B か C のどれかです」
と表現するための仕組みです。

形式的にはこうです。

from typing import Union

value: Union[int, str]
Python

これは「valueintstr のどちらか」という意味です。

Python 3.10 以降なら、もっと短くこう書けます。

value: int | str
Python

この | 記号は、型の「または(OR)」だと思ってください。
ここをしっかり理解すると、Optional も一気に腑に落ちます。


まずはシンプルな例でUnionの感覚をつかむ

数値か文字列を受け取って、文字列にする関数

例えば、「数値でも文字列でも受け取って、とにかく文字列にして返す」関数を考えます。

from typing import Union

def to_str(value: Union[int, float, str]) -> str:
    return str(value)
Python

Python 3.10 以降なら、こう書けます。

def to_str(value: int | float | str) -> str:
    return str(value)
Python

この関数の型情報から、次のことが読み取れます。

引数 value は、intfloatstr のどれか
戻り値は必ず str

つまり、「入力はゆるく、出力はカッチリ」という設計です。
呼び出す側は、「value に bool や list を渡すのは想定外なんだな」と分かります。

Unionがないとどう見えるか

型ヒントを書かないと、こうなります。

def to_str(value):
    return str(value)
Python

これだと、

何を渡していいのか
何が返ってくるのか

がコードから読み取れません。

Union を使うことで、「許される型の範囲」がはっきりします。
これは、設計とテストの両方に効いてきます。


Optionalとの関係からUnionを深く理解する

Optionalは「Union[T, None]の別名」

Optional[T] は、実はこう定義されています。

from typing import Optional, Union

Optional[str] == Union[str, None]
Python

つまり、「strNone」という Union です。

Python 3.10 以降なら、こうも書けます。

str | None  # これも同じ意味
Python

ここで大事なのは、

Union は「型を複数並べて“どれか”を許す」
Optional は「その中に None を含めるためのショートカット」

という関係です。

Union を理解すると、Optional の正体もクリアになります。


Unionがテストと品質にどう効いてくるか

「どのパターンをテストすべきか」が見えやすくなる

例えば、さっきの to_str をテストするとします。

def to_str(value: int | float | str) -> str:
    return str(value)
Python

この型を見た時点で、テストの観点が自然に出てきます。

int を渡したとき
float を渡したとき
str を渡したとき

少なくともこの 3 パターンはテストしたくなりますよね。

型ヒントがないと、「たまたま int だけテストして終わり」になりがちです。
Union は、「仕様上のバリエーション」を型として表現することで、
テストケースの抜けを減らしてくれます。

呼び出し側の「間違った使い方」を早期に検出できる

例えば、こういうコードを書いたとします。

result = to_str([1, 2, 3])
Python

Python 的には実行時までエラーになりませんが、
静的型チェッカー(mypy など)は、

to_strint | float | str しか受け取らないのに、list[int] を渡している」

と教えてくれます。

これは、「テストを書く前に、型レベルのバグを潰せる」ということです。
Union をきちんと書いておくと、
「想定していない型を渡している」ミスを早期に見つけられます。


Unionを使うときに気をつけたい設計のポイント

「なんでもUnion」は設計の負け

Union は便利ですが、
何でもかんでも Union にすると、設計が崩壊します。

例えば、こういうのは危険信号です。

from typing import Union

def process(value: Union[int, str, float, list, dict, None]) -> Union[int, str, None]:
    ...
Python

ここまで来ると、「結局何でもアリ」です。
呼び出し側も、「何を渡していいのか分からない」状態になります。

Union を使うときは、

「この関数は、なぜ複数の型を受け取る必要があるのか?」

を必ず自問してください。

本当に必要なケースもありますが、
多くの場合は「責務を分けた方がいい」サインです。

関数を分けた方がきれいなことも多い

例えば、こういう関数があったとします。

def parse_id(value: int | str) -> int:
    if isinstance(value, int):
        return value
    return int(value)
Python

これはまだ許容範囲ですが、
もしロジックが複雑になってきたら、
関数を分けた方が読みやすくなることもあります。

def parse_id_from_int(value: int) -> int:
    return value

def parse_id_from_str(value: str) -> int:
    return int(value)
Python

そして、呼び出し側で「どちらを使うか」を決める。

Union は「受け取る側の都合」で型を増やす道具ですが、
設計としては「呼び出し側で責務を分ける」方がきれいなことも多いです。

Union を使う前に、

「これは本当に一つの関数で受けるべきか?」

を一度考える癖をつけると、設計の質が上がります。


実践的な例:設定値を「文字列か数値」で受け取る

ありがちなパターン:設定ファイル由来の値

設定ファイル(例:環境変数)から値を読むとき、
文字列として来ることもあれば、
コード内では数値として扱いたいこともあります。

from typing import Union

def parse_timeout(value: Union[int, str]) -> int:
    if isinstance(value, int):
        return value
    return int(value)
Python

Python 3.10 以降なら、

def parse_timeout(value: int | str) -> int:
    if isinstance(value, int):
        return value
    return int(value)
Python

この関数の型から、次のことが分かります。

呼び出し側は、intstr を渡してよい
戻り値は必ず int

テストを書くときも、

int を渡したとき(そのまま返る)
str を渡したとき(int に変換される)
変換できない文字列を渡したとき(例外になる)

といった観点が自然に出てきます。

Union は、「現実世界の“揺れ”」を型で表現するのに向いています。


初心者がUnionとどう付き合うといいか

まずは「2種類の型」を扱うところから始める

最初から 3 つも 4 つも型を並べると、
頭がこんがらがります。

最初のステップとしては、

int | str
str | None(=Optional[str])

くらいのシンプルな Union から始めるのがちょうどいいです。

そこから、

「なぜこの関数は 2 種類の型を受け取る必要があるのか?」
「呼び出し側は、その違いを意識できているか?」

を考える癖をつけていくと、
Union の設計センスが育っていきます。

Unionは「仕様の分岐」を型で表すもの、と意識する

Union を付けるということは、

「この値には、仕様上のバリエーションがある」

と宣言することです。

そのバリエーションごとに、

どう振る舞うべきか
どうテストすべきか

を考える必要があります。

「とりあえず Union にしておけばエラーが消えるから」
という理由で使い始めると、
設計もテストも一気に崩れます。

Union は、「仕様の分岐を型で表現するための道具」だと意識して使うのが大事です。


まとめ(Unionは「型レベルの“または”」で仕様をはっきりさせる)

Union を初心者目線で整理すると、こうなります。

Union[A, B, ...](または A | B | ...)は、「この値は A か B か…のどれか」という型で、Optional はその特別版として「T か None」を表している。
Union を使うと、「この関数はどの型を受け取れるか」「どのパターンをテストすべきか」が明確になり、呼び出し側の間違った使い方も静的解析で早期に検出できる。
ただし、「なんでもかんでも Union」にすると設計が崩れるので、本当に複数の型を受け取る必然性があるときだけ使い、場合によっては関数を分けることも検討すべき。
Union は「仕様上のバリエーション」を型で表現する道具であり、それをきっかけに設計とテストの観点を深く考えられるようになると、コードの品質が一段上がる。

タイトルとURLをコピーしました