[初心者向け] Python のオーバーロード

# はじめに

Python における**オーバーロード**を解説していきます。
`@overload` と `@singledispatch` のどちらかで解説されているものが多く混乱しやすかったので、両方の性質を比較して整理していきます。

**オーバーライド**はオブジェクト指向の回で解説する予定なので、興味あればそちらもご覧ください。

:::note warn
型チェッカーを有効にしないと、解説の恩恵を受けられません
:::

# オーバーロード

引数が異なる同名の関数 (メソッド) や演算子を定義することです。**多重定義**ともいいます。

Java でのオーバーロードは、引数の構成を変えた同名の関数を定義するだけです。

```java:Java でのオーバーロード
public static int func(int x, int y){
    return x + y;
}

public static int func(int x, int y, int z){
    return x + y + z;
}

// 以下問題なく呼び出せる
func(1, 2);
func(1, 2, 3);
```

Python では同名の関数を定義すると、前に定義したものの情報が上書きされて消えてしまいます。

```python:Python でも似たようなことをやってみると...
def func(x: int, y: int) -> int:
    return x + y


def func(x: int, y: int, z: int) -> int:
    return x + y + z

# 2 つめの func が有効なので引数が 3 つ必要
func(1, 2)  # 1 つめの func の情報は存在しない
func(1, 2, 3)
```

Java ほど単純ではありませんが、Python にもオーバーロードのための記述があるので、それを解説していきます。

まず説明する[引数の型に応じた戻り値の型](#引数の型に応じた戻り値の型)は、型チェッカーに厳密性を付与するものです。
その次の[引数の型に応じた処理の分岐](#引数の型に応じた処理の分岐)が、一般的なオーバーロードのイメージに近いです。

<a id="引数の型に応じた戻り値の型"></a>

## 引数の型に応じた戻り値の型

引数の型によって、戻り値の型が変わる処理を考えます。

下では関数の引数は `int` または `str` で、戻り値の型は引数の型と同じです。
最終行において `int_or_str` の引数が `int` なので、その戻り値も同じ `int` となり、問題ないように思えます。
ただし型チェッカーは内部の処理までは見てくれず、関数定義時の型のみで判断するため、この記述では戻り値の型は `int | str` でなければならないと表示されます。

```python:引数によって戻り値の異なる関数
def int_or_str(arg: int | str) -> int | str:
    """引数が int であれば倍に、str であれば繰り返す"""
    return arg * 2

val_1:int = int_or_str(1)  # 型チェッカーによるエラーが表示
```

## overload デコレータ

`@overload` を利用すると、引数と戻り値の型を正確に紐づけられます。

```python:引数の型と戻り値の型の組み合わせの記述
from typing import overload


@overload
def func(arg: tp_1) -> tp_a:
    pass


@overload
def func(arg: tp_2) -> tp_b:
    pass


def func(arg: Union[tp_1, tp_2]):
    ...
```

`@overload` でのオーバーロードを定義する手順は以下の通りです。

1. 複数の引数の型を取りうる関数を定義
2. 1 の関数の上に `@overload` を付与した同名の関数を定義
3. 2 の関数の引数と戻り値に型ヒントを付与して、`pass` と記述
4. 紐づけたい引数と戻り値の型の組み合わせの数だけ 2 ~ 3 を繰り返す

さきほどの例を `@overload` を利用して記述すると、以下のようになります。

```python:正確な引数の型と戻り値の型の紐づけ
@overload
def int_or_str(arg: int) -> int:
    pass


@overload
def int_or_str(arg: str) -> str:
    pass


def int_or_str(arg: int | str) -> int | str:
    return arg * 2


# 引数の型から戻り値の型が正確に推定
val_2: int = int_or_str(1)  # 戻り値は int 型
val_3: str = int_or_str("a")  # 戻り値は str 型
```

`@overload` が付与されている関数は、型チェッカーのために利用されるもので、中の処理は無視されます。
つまり型チェッカーを黙らせる機能でしかないため、引数の型によって処理を変えるようなことはできません。

引数の型に応じた処理を行うには `@singledispatch` を利用します。

<a id="引数の型に応じた処理の分岐"></a>

## 引数の型に応じた処理の分岐

引数の型によって処理を分岐させるケースを考えます。

下では引数が `int` か `str` で、処理を変えています。型の判定のためだけに `if` を使用しており、可読性が微妙です。

```python:原始的な引数の型に応じた処理の分岐
def twins(arg: int | str):
    """引数の型によって異なる処理"""
    if type(arg) is int:
        # 年齢を倍にして表示
        print(f"age : {arg*2}")
    elif type(arg) is str:
        # 名前を繰り返し表示
        print(f"name : {arg*2}")


twins(28)
twins("ABC")

--> age : 56
    name : ABCABC
```

## singledispatch デコレータ

`@singledispatch` を利用すると、内部の処理がスッキリして可読性が向上します。

```python:引数の型に応じた処理の分岐
from functools import singledispatch


@singledispatch
def func(arg: tp_1):
    A


@func.register
def _(arg: tp_2):
    B
```

`@singledispatch` でのオーバーロードの手順は以下の通りです。

1. `@singledispatch` を付与した関数を型ヒントも記述して定義
2. 関数名と `.register`から成るデコレータを付与して、関数名を `_` で定義
3. 2 の関数に型ヒントと引数の型に応じた処理を記述
4. 引数の型とそれに紐づいた処理の組み合わせの数だけ 2 ~ 3 を繰り返す

さきほどの例を `singledispatch` を利用して記述すると、以下のようになります。

```python:可読性の高い引数の型に応じた処理の分岐
@singledispatch
def twins(age: int):
    print(f"age : {age * 2}")


@twins.register
def _(name: str):
    print(f"name : {name * 2}")


twins(28)  # 引数が int → 上の関数
twins("ABC")  # 引数が str → 下の関数
```

`@singledispatch` のほうが、汎用的に思えます。

## 補足

今回のケースでは引数と戻り値の型が同一であるため、ジェネリクスでも代用できます。いずれジェネリクスも解説します。

## まとめ

型チェッカーを黙らせる場合は `@overload`、処理を変える場合は `@singledispatch` を利用してください。

# おわりに

年明け転職しまして、かなり忙しくなりました。4 月に入りようやく落ち着いたので、記事の投稿を再開します。

# 更新履歴

2025/04/05 初版
2025/04/24 違いをより明確にするような説明を追加