はじめに
Python におけるオーバーロードを解説していきます。
@overload
と @singledispatch
のどちらかで解説されているものが多く混乱しやすかったので、両方の性質を比較して整理していきます。
オーバーライドはオブジェクト指向の回で解説する予定なので、興味あればそちらもご覧ください。
型チェッカーを有効にしないと、解説の恩恵を受けられません
オーバーロード
引数が異なる同名の関数 (メソッド) や演算子を定義することです。多重定義ともいいます。
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 では同名の関数を定義すると、前に定義したものの情報が上書きされて消えてしまいます。
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 にもオーバーロードのための記述があるので、それを解説していきます。
まず説明する引数の型に応じた戻り値の型は、型チェッカーに厳密性を付与するものです。
その次の引数の型に応じた処理の分岐が、一般的なオーバーロードのイメージに近いです。
引数の型に応じた戻り値の型
引数の型によって、戻り値の型が変わる処理を考えます。
下では関数の引数は int
または str
で、戻り値の型は引数の型と同じです。
最終行において int_or_str
の引数が int
なので、その戻り値も同じ int
となり、問題ないように思えます。
ただし型チェッカーは内部の処理までは見てくれず、関数定義時の型のみで判断するため、この記述では戻り値の型は int | str
でなければならないと表示されます。
def int_or_str(arg: int | str) -> int | str:
"""引数が int であれば倍に、str であれば繰り返す"""
return arg * 2
val_1:int = int_or_str(1) # 型チェッカーによるエラーが表示
overload デコレータ
@overload
を利用すると、引数と戻り値の型を正確に紐づけられます。
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 の関数の上に
@overload
を付与した同名の関数を定義 - 2 の関数の引数と戻り値に型ヒントを付与して、
pass
と記述 - 紐づけたい引数と戻り値の型の組み合わせの数だけ 2 ~ 3 を繰り返す
さきほどの例を @overload
を利用して記述すると、以下のようになります。
@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
を利用します。
引数の型に応じた処理の分岐
引数の型によって処理を分岐させるケースを考えます。
下では引数が int
か str
で、処理を変えています。型の判定のためだけに if
を使用しており、可読性が微妙です。
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
を利用すると、内部の処理がスッキリして可読性が向上します。
from functools import singledispatch
@singledispatch
def func(arg: tp_1):
A
@func.register
def _(arg: tp_2):
B
@singledispatch
でのオーバーロードの手順は以下の通りです。
-
@singledispatch
を付与した関数を型ヒントも記述して定義 - 関数名と
.register
から成るデコレータを付与して、関数名を_
で定義 - 2 の関数に型ヒントと引数の型に応じた処理を記述
- 引数の型とそれに紐づいた処理の組み合わせの数だけ 2 ~ 3 を繰り返す
さきほどの例を singledispatch
を利用して記述すると、以下のようになります。
@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 違いをより明確にするような説明を追加