はじめに
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を検討していただければと思います。