15 votes

Rendre la surcharge des opérateurs moins redondante en Python?

Je suis en train d'écrire une classe surchargeant le type liste. Je viens juste d'écrire ça et je me demande s'il existe une autre façon moins redondante de le faire :

class Vector:
    def __mul__(self, other):
        #Vector([1, 2, 3]) * 5 => Vector([5, 10, 15])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i * other)
            return Vector(tmp)
        raise VectorException("Nous ne pouvons multiplier qu'un vecteur par un scalaire")

    def __truediv__(self, other):
        #Vector([1, 2, 3]) / 5 => Vector([0.2, 0.4, 0.6])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i / other)
            return Vector(tmp)
        raise VectorException("Nous ne pouvons diviser qu'un vecteur par un scalaire")

    def __floordiv__(self, other):
        #Vector([1, 2, 3]) // 2 => Vector([0, 1, 1])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i // other)
            return Vector(tmp)
        raise VectorException("Nous ne pouvons diviser qu'un vecteur par un scalaire")

Comme vous pouvez le voir, chaque méthode surchargée est un copier/coller de la précédente avec juste de petits changements.

19voto

Ramazan POLAT Points 475

Factoriser le code en utilisant le patron de conception décorateur et la fonction lambda:

class Vector:
    def __do_it(self, other, func):
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(func(i, other))
            return Vector(tmp)
        raise ValueError("Nous ne pouvons opérer un Vecteur que par un scalaire")

    def __mul__(self, other):
        return self.__do_it(other, lambda i, o: i * o)

    def __truediv__(self, other):
        return self.__do_it(other, lambda i, o: i / o)

    def __floordiv__(self, other):
        return self.__do_it(other, lambda i, o: i // o)

18voto

abarnert Points 94246

Ce que vous voulez faire ici est de générer dynamiquement les méthodes. Il existe plusieurs façons de le faire, en allant de super dynamique et en les créant à la volée dans un __getattribute__ de métaclass (bien que cela ne fonctionne pas pour certaines méthodes spéciales - voir la documentation) à la génération de texte source à enregistrer dans un fichier .py que vous pouvez ensuite importer. Mais la solution la plus simple est de les créer dans la définition de la classe, quelque chose comme ceci :

class MyVector :
    # ...

    def _make_op_method(op):
        def _op(self, other):
            if isinstance(other, int) or isinstance(other, float):
                tmp = list()
                for i in self.l:
                    tmp.append(op(i, other))
                return Vector(tmp)
            raise VectorException("Nous ne pouvons que {} un vecteur par un scalaire".format(
                op.__name__.strip('_'))
        _op.__name__ = op.__name__
        return _op

    __mul__ = _make_op(operator.__mul__)
    __truediv__ = _make_op(operator.__truediv__)
    # et ainsi de suite

Vous pouvez devenir plus sophistiqué et définir _op.__doc__ sur un docstring approprié que vous générez (voir functools.wraps dans le module stdlib pour un code pertinent), et construire __rmul__ et __imul__ de la même manière que vous construisez __mul__, et ainsi de suite. Et vous pouvez écrire une métaclass, un décorateur de classe ou un générateur de fonction qui encapsule certains détails si vous devez faire de nombreuses variations de la même chose. Mais c'est l'idée de base.

En fait, en le déplaçant en dehors de la classe, il est plus facile d'éliminer encore plus de répétitions. Définissez simplement cette méthode _op(self, other, op) dans la classe au lieu de localement à l'intérieur de _make_op et décorez la classe avec @numeric_ops , que vous pouvez définir comme ceci :

def numeric_ops(cls):
    for op in "mul truediv floordiv".split():  # "mul truediv floordiv ... ".split():
        def _op(self, other):
            return self._op(other, getattr(operator, op))
        _op.__name__ = f"__{op}__"
        setattr(cls, f"__{op}__", _op)

Si vous regardez, par exemple, functions.total_ordering, il fait quelque chose de similaire pour générer tous les opérateurs d'ordonnancement manquants parmi ceux qui sont présents.

Les operator.mul, etc., viennent du module operator dans le stdlib - ce ne sont que des fonctions triviales où operator.__mul__(x, y) appelle en gros x * y, et ainsi de suite, faites pour quand vous avez besoin de passer une expression d'opérateur comme fonction.

Il existe quelques exemples de ce genre de code dans le stdlib - bien que beaucoup plus d'exemples du __rmul__ = __mul__ lié mais beaucoup plus simple.

La clé ici est qu'il n'y a pas de différence entre les noms que vous créez avec def et les noms que vous créez en assignant avec =. De toute façon, __mul__ devient un attribut de la classe, et sa valeur est une fonction qui fait ce que vous voulez. (Et, de même, il n'y a presque pas de différence entre les noms que vous créez pendant la définition de la classe et les noms que vous injectez après.)


Alors, devriez-vous faire cela ?

Eh bien, le DRY est important. Si vous copiez-collez-modifiez une douzaine de fois, il n'est pas improbable que vous vous trompiez dans l'une des modifications et vous retrouviez avec une méthode mod qui multiplie en réalité et cela (et un test unitaire qui ne le détecte pas). Et puis, si plus tard vous découvrez un défaut dans l'implémentation que vous avez copiée-collée une douzaine de fois (comme entre la version originale et la version modifiée de la question), vous devez corriger le même défaut à douze endroits, ce qui est un autre aimant potentiel de bogues.

D'un autre côté, la lisibilité compte. Si vous ne comprenez pas comment cela fonctionne, vous ne devriez probablement pas le faire, et devriez vous contenter de la réponse de Ramazan Polat. (Ce n'est pas tout à fait aussi compact, ou aussi efficace, mais c'est certainement plus facile à comprendre.) Après tout, si le code n'est pas évident pour vous, le fait que vous n'ayez à corriger un bogue qu'une seule fois au lieu de douze est balayé par le fait que vous ne savez pas comment le corriger. Et même si vous le comprenez, le coût de la subtilité peut souvent l'emporter sur les avantages du DRY.

Je pense que total_ordering montre à peu près où vous voudriez tracer les limites. Si vous le faites une fois, vous êtes mieux avec la répétition, mais si vous le faites pour plusieurs classes ou dans plusieurs projets, vous êtes probablement mieux en abstrayant la subtilité dans une bibliothèque que vous pouvez écrire une fois, tester de manière exhaustive avec une variété de classes différentes, et ensuite utiliser maintes et maintes fois.

9voto

DYZ Points 26904

Votre code pourrait être aussi compact que ci-dessous (juanpa.arrivillaga a suggéré de return NotImplemented au lieu de lever une exception):

def __mul__(self, other):
    #Vector([1, 2, 3]) * 5 => Vector([5, 10, 15])
    try:
        return Vector([i * other for i in self.l])
    except TypeError:
        return NotImplemented

7voto

munkhd Points 1876

Le modèle Strategy est votre ami ici. Je vais également aborder quelques autres façons de nettoyer le code.

Vous pouvez lire à propos du modèle de stratégie ici : https://en.wikipedia.org/wiki/Strategy_pattern

Vous avez dit "Comme vous pouvez le voir, chaque méthode surchargée est une copie/collage de la précédente avec juste de petits changements." C'est un signe pour utiliser ce modèle. Si vous pouvez transformer le petit changement en une fonction, alors vous pouvez écrire une seule fois le code de base et vous concentrer sur les parties intéressantes.

class Vector:
    def _arithmitize(self, other, f, error_msg):
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for a in self.l:
                tmp.append(func(a, other))
            return Vector(tmp)
        raise ValueError(error_msg)

    def _err_msg(self, op_name):
        return "Nous ne pouvons que {} un vecteur par un scalaire".format(opp_name)

    def __mul__(self, other):
        return self._arithmitize(
            other, 
            lambda a, b: a * b, 
            self._err_msg('mul'))

    def __div__(self, other):
        return self._arithmitize(
            other, 
            lambda a, b: a / b, 
            self._err_msg('div'))
    # et ainsi de suite ...

Nous pouvons encore nettoyer un peu plus avec une compréhension de liste

class Vector:
    def _arithmetize(self, other, f, error_msg):
        if isinstance(other, int) or isinstance(other, float):
            return Vector([f(a, other) for a in self.l])
        raise ValueError(error_msg)

    def _err_msg(self, op_name):
        return "Nous ne pouvons que {} un vecteur par un scalaire".format(opp_name)

    def __mul__(self, other):
        return self._arithmetize(
            other, 
            lambda a, b: a * b, 
            self._err_msg('mul'))

    def __div__(self, other):
        return self._arithmetize(
            other, 
            lambda a, b: a / b, 
            self._err_msg('div'))

Nous pouvons améliorer la vérification des types

import numbers

class Vector:
    def _arithmetize(self, other, f, error_msg):
        if isinstance(other, number.Numbers):
            return Vector([f(a, other) for a in self.l])
        raise ValueError(error_msg)

Nous pouvons utiliser des opérateurs au lieu d'écrire des lambdas :

import operators as op

class Vector:
    # snip ...
    def __mul__(self, other):
        return self._arithmetize(other, op.mul, self._err_msg('mul'))

Donc au final, nous obtenons quelque chose comme cela :

import numbers
import operators as op

class Vector(object):
    def _arithmetize(self, other, f, err_msg):
        if isinstance(other, numbers.Number):
            return Vector([f(a, other) for a in self.l])
        raise ValueError(self._error_msg(err_msg))
    def _error_msg(self, msg):
        return "Nous ne pouvons que {} un vecteur par un scalaire".format(opp_name)

    def __mul__(self, other):
        return self._arithmetize(op.mul, other, 'mul')

    def __truediv__(self, other):
        return self._arithmetize(op.truediv, other, 'truediv')

    def __floordiv__(self, other):
        return self._arithmetize(op.floordiv, other, 'floordiv')

    def __mod__(self, other):
        return self._arithmetize(op.mod, other, 'mod')

    def __pow__(self, other):
        return self._arithmetize(op.pow, other, 'pow')

Il y a d'autres moyens de générer dynamiquement cela, mais pour un petit ensemble de fonctions comme celui-ci, la lisibilité compte.

Si vous devez générer cela dynamiquement, essayez quelque chose comme ceci :

class Vector(object):
    def _arithmetize(....):
        # vous avez déjà vu cela 

    def __getattr__(self, name):
        funcs = {
            '__mul__': op.mul, # note: cela peut ne pas fonctionner avec les méthodes d'attributs spéciaux. À vérifier
            '__mod__': op.mod,
            ...
        }
        def g(self, other):
            try:
                return self._arithmetize(funcs[name],...)
             except:
                 raise NotImplementedError(...)
        return g

Si vous trouvez que cet exemple dynamique ne fonctionne pas, consultez rendre la surcharge d'opérateurs moins redondante en python?, qui traite du cas de la création dynamique de méthodes d'attributs spéciaux dans la plupart des implémentations python.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X