LoginSignup
52
56

PythonのUnionをなるべく使わないで欲しい件について

Last updated at Posted at 2024-04-07

はじめに

Pythonの型ヒントにおいて、Unionは複数の型を受け入れることができる便利な機能です。しかし、Unionの過度な使用は、コードの可読性や保守性を低下させる可能性があります。本記事では、Unionをなるべく使わないようにすべき理由について説明します。

内容

ここでは2つのUnionを使って欲しくないケースについて説明します。

ケース1

main_union.py
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

上記についてあまり問題に思わない方もいるかもしれませんが、変数の役割を考えた時に、変数の型の変化を許容した方が良いケースは稀有なのかなと思います。(筆者は入力時点で型を複数許容するケースは良いと思っています。)

変数の型の変化を許容しない場合は、ジェネリック型を用いて以下のように書けます。

main_typevar.py
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が使われると想定される例として、辞書型のケースを紹介します。

main_union_dcit.py
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

上記の問題はnameint型が入るケースとagestr型の変数が入るケースを静的解析で対策できないことです。

Unionを使わずにdataclassなどを使うと変数の型の変化を許容しません。

main_dataclass.py
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を検討していただければと思います。

52
56
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
56