12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonでメソッドのオーバーロードをする

Last updated at Posted at 2021-01-31

はじめに

Pythonにはメソッドのオーバーロードが標準にはありません.
これは型がないことに影響します.
型がないので呼び出すメソッドが判断できないということです.

通常の実装だと複数の型を受け取れるように関数内で工夫しているかと思います.
(if文で処理を分けてキャストしたりなど)
本質的な実装ではないので,個人的には避けたいです.

関数のオーバーロード自体はPython3.4から追加されたsingledispatchを使うことで実現できます.
これは型アノテーションを使うことを前提としています.

これは関数なのでメソッドではないです.
メソッドで実現したいことが多いので,singledispatchを拡張して実装しました.

実装

以下のデコレータ関数で実現します.

funcdispatch.py
from pathlib import Path
from typing import get_type_hints, _GenericAlias
from functools import singledispatch, update_wrapper

def funcdispatch(method: bool = True):
    """
    メソッド用のオーバーロード実装
    Args:
        method: メソッドかどうか
    """

    def dispach(func):
        # 通常のsingledispatchを取得
        dispatcher = singledispatch(func)

        # regist関数リスト
        funclist = [func]

        def register(cls, func=None):
            """
            registerのオーバーライド
            registされる関数を保持しておく
            """
            # typingの型ではsingledispatchのregister時にtypeと判定されずエラーになる
            # funcがNoneの際にエラーになるので,予めclsとfuncを取得して変換する
            clses = [cls]
            if func is None:
                ann = getattr(cls, '__annotations__', {})
                if ann:
                    func = cls
                    _, cls = next(iter(get_type_hints(func).items()))

                    clses = []
                    if isinstance(cls, _GenericAlias):
                        clses.extend(cls.__args__)
                    else:
                        clses.append(cls)
            # 関数登録
            for cls in clses:
                func = dispatcher.register(cls, func)   # singledispatchのregisterを呼んでおく
            funclist.append(func)
            return func

        def wrapper(*args, **kwargs):
            """
            ラップ
            registした関数から一致する型を見つける
            """
            # デフォルトは第1引数
            # インスタンスメソッドの場合はseldになる
            dispatch_class = args[0].__class__

            # キーワード引数のみで指定されている場合は,registされた関数からクラスを取得
            args_is_none = (not method and len(args) == 0) or (method and len(args) == 1)
            if args_is_none and len(kwargs.keys()) > 0:
                for func in funclist:
                    # 型ヒントから第2引数名を取得して,キーワード引数から値を取得
                    # selfには型ヒントが付いていない想定なので第2引数
                    argname, _ = next(iter(get_type_hints(func).items()))

                    # 値が存在しない場合は、次の関数を探索
                    if not argname in kwargs:
                        continue

                    # 値のクラスを取得
                    arg = kwargs.get(argname, None)
                    dispatch_class = arg.__class__
                    break

            # 通常の引数が指定されている場合は,メソッド種類に従い処理
            else:
                # インスタンスメソッドの場合は第2引数を取得
                if method:
                    dispatch_class = args[1].__class__

                # 派生クラスでは判定できないので、Pathは基底クラスに変換
                if issubclass(dispatch_class, Path):
                    dispatch_class = Path

            return dispatcher.dispatch(dispatch_class)(*args, **kwargs)

        wrapper.register = register
        update_wrapper(wrapper, func)
        return wrapper
    return dispach

selfを除く第一引数の型で判断しています.
一致するものがない場合は,一番最初に登録されたメソッドが呼び出されます.

テスト

メソッドをオーバーロード

使い方はsingledispatchと同じです.

from funcdispatch import funcdispatch

class Test:

    @funcdispatch()
    def test(self, value: int):
        print("intです", value)

    @test.register
    def _(self, value: str):
        print("strです", value)


if __name__ == "__main__":
    test_obj = Test()

    # 引数がint
    test_obj.test(5)

    # 引数がstr
    test_obj.test("method overload")

intです 5
strです method overload

関数をオーバーロード

funcdispatchの第一引数でメソッドか関数か指定できます(デフォルトはTrue=メソッド).

from funcdispatch import funcdispatch

@funcdispatch(method=False)
def test(value: int):
    print("intです", value)


@test.register
def _(value: str):
    print("strです", value)


if __name__ == "__main__":
    # 引数がint
    test(5)

    # 引数がstr
    test("method overload")

intです 5
strです method overload

まとめ

Pythonでメソッドをオーバーロードする方法を紹介しました.

欠点がいくつかあるので改善中です.

  • 第一引数でしか判断できない
  • インテリセンスで型情報が見えなくなる(overloadとの組み合わせで回避できないか検討中です)

型が違うなら挙動が違うから名前を変えろよ,というのがPythonの思想ですが...
どうしても名前を変えたくない・名前を考えるのが面倒という方にオススメです.

12
7
0

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
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?