web-dev-qa-db-ja.com

インスタンスメソッドでfunctools.singledispatchを使用するにはどうすればよいですか?

Python 3.4 追加 静的メソッドで関数のオーバーロードを定義する機能。これは基本的にドキュメントの例です:

from functools import singledispatch


class TestClass(object):
    @singledispatch
    def test_method(arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    @test_method.register(int)
    def _(arg):
        print("Strength in numbers, eh?", end=" ")
        print(arg)

    @test_method.register(list)
    def _(arg):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, elem)

if __name__ == '__main__':
    TestClass.test_method(55555)
    TestClass.test_method([33, 22, 11])

最も純粋な形式では、singledispatch実装は型を識別するために最初の引数に依存するため、この機能をインスタンスメソッドに拡張するのは困難です。

インスタンス機能で動作させるためにこの機能を使用する(またはjerry-rig)方法についてアドバイスはありますか?

42
Dustin Oprea

更新:現在Python 3.8、 functools.singledispatchmethod は、メソッド、クラスメソッド、抽象メソッド、および静的メソッド。

古いPythonバージョンについては、この回答の残りを参照してください。

singledispatchの-​​ source を見ると、デコレータが関数wrapper()を返すことがわかります。これは、 args[0] ...

    def wrapper(*args, **kw):
        return dispatch(args[0].__class__)(*args, **kw)

...これは通常の関数では問題ありませんが、インスタンスメソッドではあまり使用されません。最初の引数は常にselfになります。

ただし、新しいデコレータmethdispatchを作成できます。これは、singledispatchに依存して重い処理を実行しますが、代わりに、args[1]のタイプに基づいて呼び出す登録済み関数を選択するラッパー関数を返します:

from functools import singledispatch, update_wrapper

def methdispatch(func):
    dispatcher = singledispatch(func)
    def wrapper(*args, **kw):
        return dispatcher.dispatch(args[1].__class__)(*args, **kw)
    wrapper.register = dispatcher.register
    update_wrapper(wrapper, func)
    return wrapper

以下は、使用中のデコレータの簡単な例です。

class Patchwork(object):

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

    @methdispatch
    def get(self, arg):
        return getattr(self, arg, None)

    @get.register(list)
    def _(self, arg):
        return [self.get(x) for x in arg]

装飾されたget()メソッドとlistに登録されたメソッドの両方に、通常のように初期self引数があることに注意してください。

Patchworkクラスのテスト:

>>> pw = Patchwork(a=1, b=2, c=3)
>>> pw.get("b")
2
>>> pw.get(["a", "c"])
[1, 3]
65
Zero Piraeus

デコレータは基本的に、ラップされた関数を引数として取り、別の関数を返すラッパーです。

受け入れられた回答で述べたように、singledispatchは、最初の引数を登録済みの型として取るwrapperを返します-インスタンスメソッドではself

その答えに示されているように、このような場合は、別のラッパーを記述してデコレーターにサルのパッチを適用できます。しかし、この種のハック修正は常に最良の選択肢とは限りません。

他の関数と同様に、ラッパーを呼び出して引数を明示的に渡すことができます。これは、この種のメソッドのオーバーロードがパッケージ内で行われることがほとんどない場合は、より単純でフラットで読みやすいように見えます。

from functools import singledispatch

class TestClass(object):

    def __init__(self):
        self.test_method = singledispatch(self.test_method)
        self.test_method.register(int, self._test_method_int)
        self.test_method.register(list, self._test_method_list)

    def test_method(self, arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    def _test_method_int(self, arg):
        print("Strength in numbers, eh?", end=" ")
        print(arg)

    def _test_method_list(self, arg):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, elem)


if __name__ == '__main__':
    test = TestClass()
    test.test_method(55555)
    test.test_method([33, 22, 11])

別のモジュール multipledispatch (標準ではありませんが、Anacondaに含まれており、非標準の依存関係はありません)です。名前がすでに示しているように、singledispatchとは異なり、マルチメソッドを許可します。

Dispatcherオブジェクトに加えて、singledispatch互換の構文で、これらのオブジェクトの作成と操作をユーザーから隠すdispatchデコレーターを提供します。

ディスパッチデコレータは、関数の名前を使用して、新しい署名/関数を追加する適切なDispatcherオブジェクトを選択します。新しい関数名を検出すると、新しいDispatcherオブジェクトを作成し、名前とDispatcherのペアを将来の参照のために名前空間に格納します。

例えば:

from types import LambdaType
from multipledispatch import dispatch

class TestClass(object):

    @dispatch(object)
    def test_method(self, arg, verbose=False):
        if verbose:
            print("Let me just say,", end=" ")

        print(arg)

    @dispatch(int, float)
    def test_method(self, arg, arg2):
        print("Strength in numbers, eh?", end=" ")
        print(arg + arg2)

    @dispatch((list, Tuple), LambdaType, type)
    def test_method(self, arg, arg2, arg3):
        print("Enumerate this:")

        for i, elem in enumerate(arg):
            print(i, arg3(arg2(elem)))


if __name__ == '__main__':

    test = TestClass()
    test.test_method(55555, 9.5)
    test.test_method([33, 22, 11], lambda x: x*2, float)
11
Nuno André