web-dev-qa-db-fra.com

Comment puis-je utiliser functools.singledispatch avec des méthodes d'instance?

Python 3.4 ajouté la possibilité de définir la surcharge de fonctions avec des méthodes statiques. C'est essentiellement l'exemple de la documentation:

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])

Dans sa forme la plus pure, l'implémentation singledispatch s'appuie sur le premier argument pour identifier le type, ce qui complique l'extension de cette fonctionnalité aux méthodes d'instance.

Quelqu'un a-t-il des conseils sur la façon d'utiliser (ou de jerry-rig) cette fonctionnalité pour la faire fonctionner avec les méthodes d'instance?

42
Dustin Oprea

Mise à jour: À partir de Python 3.8, functools.singledispatchmethod permet l'envoi unique sur les méthodes, les méthodes de classe, les méthodes abstraites et les méthodes statiques.

Pour les anciennes versions Python, voir le reste de cette réponse.

En regardant source pour singledispatch, nous pouvons voir que le décorateur retourne une fonction wrapper(), qui sélectionne une fonction à appeler parmi celles enregistrées en fonction du type de args[0] ...

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

... ce qui est bien pour une fonction régulière, mais peu utile pour une méthode d'instance, dont le premier argument sera toujours self.

Nous pouvons cependant écrire un nouveau décorateur methdispatch, qui s'appuie sur singledispatch pour faire le gros du travail, mais renvoie à la place une fonction wrapper qui sélectionne la fonction enregistrée à appeler en fonction du type de 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

Voici un exemple simple du décorateur utilisé:

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]

Notez que la méthode décorée get() et la méthode enregistrée dans list ont un argument initial self comme d'habitude.

Test de la classe Patchwork:

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

Un décorateur est essentiellement un wrapper qui prend la fonction encapsulée comme argument et renvoie une autre fonction.

Comme indiqué dans la réponse acceptée, singledispatch renvoie un wrapper qui prend le premier argument comme type enregistré - self dans les méthodes d'instance.

Comme indiqué dans cette réponse, dans des cas comme celui-ci, vous pouvez écrire un autre wrapper pour patcher le décorateur. Mais ce type de correctifs hacky n'est pas toujours la meilleure option.

Comme pour toute autre fonction, vous pouvez appeler l'encapsuleur et lui passer explicitement les arguments, ce qui me semble plus simple, plus plat et plus lisible si ce type de surcharge de méthode n'est que rarement effectué dans un package.

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])

Il existe un autre module, multipledispatch (non standard mais inclus dans Anaconda et sans dépendances non standard) qui, comme son nom l'indique déjà et contrairement à singledispatch, permet des méthodes multiples .

En plus des objets Dispatcher, avec une syntaxe compatible singledispatch, il fournit un décorateur dispatch qui masque la création et la manipulation de ces objets à l'utilisateur.

Le décorateur de répartition utilise le nom de la fonction pour sélectionner l'objet Dispatcher approprié auquel il ajoute la nouvelle signature/fonction. Lorsqu'il rencontre un nouveau nom de fonction, il crée un nouvel objet Dispatcher et stocke la paire nom/Dispatcher dans un espace de noms pour référence future.

Par exemple:

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é