はじめに
Pythonの型ヒントにおいて、Union
は複数の型を受け入れることができる便利な機能です。しかし、Union
の過度な使用は、コードの可読性や保守性を低下させる可能性があります。本記事では、Union
をなるべく使わないようにすべき理由について説明します。
内容
ここでは2つのUnion
を使って欲しくないケースについて説明します。
ケース1
from typing import Union
U = Union[str, int] # str | int でも可
def test_union_change(p: U):
if isinstance(p, str):
p = 1
v = "1"
test_union_change(v)
上記のプログラムに対してmypy
で静的解析すると、もちろん通ります。
% mypy main_union.py
Success: no issues found in 1 source file
上記についてあまり問題に思わない方もいるかもしれませんが、変数の役割を考えた時に、変数の型の変化を許容した方が良いケースは稀有なのかなと思います。(筆者は入力時点で型を複数許容するケースは良いと思っています。)
変数の型の変化を許容しない場合は、ジェネリック型を用いて以下のように書けます。
from typing import TypeVar
T = TypeVar("T", str, int)
def test_typevar_change(p: T):
if isinstance(p, str):
p = 1
v: str = "1"
test_typevar_change(v)
上記のプログラムに対してmypy
で静的解析すると、エラーを返してくれます。
% mypy main_typevar.py
main_typevar.py:7: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment]
Found 1 errors in 1 file (checked 1 source file)
ケース2
次にUnion
が使われると想定される例として、辞書型のケースを紹介します。
from typing import Union
U = Union[str, int]
def test_union_change(p: U):
if isinstance(p, str):
p = 1
d: dict[str, U] = {'name': 'Alice', 'age': 20}
test_union_change(d['name'])
上記のプログラムに対してmypy
で静的解析すると通ります。
% mypy main_union_dict.py
Success: no issues found in 1 source file
上記の問題はname
にint
型が入るケースとage
にstr
型の変数が入るケースを静的解析で対策できないことです。
Union
を使わずにdataclass
などを使うと変数の型の変化を許容しません。
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
def test_dataclass_change(person: Person):
if isinstance(person.name, str):
person.name = 1
alice = Person(name='Alice', age=20)
test_dataclass_change(alice)
上記のプログラムに対してmypy
で静的解析すると、エラーを返してくれます。
% mypy main_dataclass.py
main_dataclass.py:10: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment]
Found 1 error in 1 file (checked 1 source file)
Union
を使わなければいけないケース/使った方がいいケース
上記でUnion
を使わずに、ジェネリック型やdataclass
を使って欲しいと書きましたが、Union
を使わなければならないケースもあります。
例えば返り値がUnion
になっている既存ライブラリや過去実装を使うケースです。
次にdataclass
を定義している時間がないというケースです。ただこのケースでUnion
を使っているのならば、静的解析をしている意味が半減していると思いますし、そもそもスピード重視ならばUnion
などの型アシスト自体をつけない方が良いんじゃなかろうか、というのが個人的な意見です。(一方でスピード重視と型重視の中間でUnion
を使うというのが主張な気もしますし、一定理解はできます。)
最後に@Yosh31207様からコメントいただいたUnion
を使った方が良いケースについて紹介します。
Command = Union[Move, Stop, Attack] # Move | Stop | Attack
def make_commands() -> list[Command]:
上記例のように複数の型を許容する配列に関しては、ジェネリックスは適さず、Union
を使った方が良いです。
(Move | Stop | Attack
の上位クラスを定義し、その上位クラスを型アシストで用いた場合、Union
よりも型安全性が落ちてしまうので、Union
が最適となります。)
@Yosh31207様、ありがとうございます!
おわりに
いかがだったでしょうか。
Union
を使わない方がよい場面では、ジェネリック型やdataclass
を検討していただければと思います。