web-dev-qa-db-fra.com

__lt__ au lieu de __cmp__

Python 2.x a deux façons de surcharger les opérateurs de comparaison, __cmp__ ou les "opérateurs de comparaison riches" tels que __lt__ . Les riches surcharges de comparaison seraient préférées, mais pourquoi en est-il ainsi?

Les opérateurs de comparaison riches sont plus simples à implémenter chacun, mais vous devez implémenter plusieurs d'entre eux avec une logique presque identique. Cependant, si vous pouvez utiliser le tri intégré cmp et le triplet, alors __cmp__ devient assez simple et remplit toutes les comparaisons:

class A(object):
  def __init__(self, name, age, other):
    self.name = name
    self.age = age
    self.other = other
  def __cmp__(self, other):
    assert isinstance(other, A) # assumption for this example
    return cmp((self.name, self.age, self.other),
               (other.name, other.age, other.other))

Cette simplicité semble mieux répondre à mes besoins que de surcharger les 6 (!) Des comparaisons riches. (Cependant, vous pouvez le ramener à "juste" 4 si vous vous fiez à "l'argument échangé"/au comportement réfléchi, mais cela se traduit par une nette augmentation des complications, à mon humble avis.)

Y a-t-il des pièges imprévus dont je dois être informé si je ne surcharge que __cmp__?

Je comprends le <, <=, ==, etc., les opérateurs peuvent être surchargés à d'autres fins et peuvent renvoyer tout objet qu'ils souhaitent. Je ne pose pas de question sur les mérites de cette approche, mais seulement sur les différences lors de l'utilisation de ces opérateurs pour des comparaisons dans le même sens qu'ils signifient pour les nombres.

Mise à jour: Comme Christopher l'a souligné , cmp disparaît dans 3.x. Existe-t-il des alternatives qui facilitent la mise en œuvre des comparaisons comme ci-dessus __cmp__?

95
Roger Pate

Oui, il est facile de tout mettre en œuvre, par exemple __lt__ avec une classe mixin (ou une métaclasse, ou un décorateur de classe si votre goût fonctionne de cette façon).

Par exemple:

class ComparableMixin:
  def __eq__(self, other):
    return not self<other and not other<self
  def __ne__(self, other):
    return self<other or other<self
  def __gt__(self, other):
    return other<self
  def __ge__(self, other):
    return not self<other
  def __le__(self, other):
    return not other<self

Maintenant, votre classe peut définir simplement __lt__ et multipliez héritez de ComparableMixin (après toutes les autres bases dont il a besoin, le cas échéant). Un décorateur de classe serait assez similaire, insérant simplement des fonctions similaires en tant qu'attributs de la nouvelle classe qu'il décorait (le résultat pourrait être microscopiquement plus rapide à l'exécution, à un coût tout aussi minime en termes de mémoire).

Bien sûr, si votre classe a un moyen particulièrement rapide à implémenter (par exemple) __eq__ et __ne__, il doit les définir directement pour que les versions du mixin ne soient pas utilisées (par exemple, c'est le cas pour dict) - en fait __ne__ pourrait bien être défini pour faciliter cela:

def __ne__(self, other):
  return not self == other

mais dans le code ci-dessus, je voulais garder la symétrie agréable de n'utiliser que < ;-). Quant à savoir pourquoi __cmp__ devait partir, car nous avions __lt__ et mes amis, pourquoi garder une autre façon différente de faire exactement la même chose? C'est juste tellement de poids mort dans tous les Python runtime (Classic, Jython, IronPython, PyPy, ...). Le code qui certainement n'aura pas de bugs est le code qui n'est pas là - d'où le principe de Python selon lequel il devrait y avoir idéalement une manière évidente d'effectuer une tâche (C a le même principe dans la section "Spirit of C" de la norme ISO, btw).

Cela ne signifie pas que nous nous efforçons d'interdire les choses (par exemple, la quasi-équivalence entre les mixins et les décorateurs de classe pour certaines utilisations), mais cela signifie certainement ne signifie que nous n'aimons pas porter autour de code dans les compilateurs et/ou runtimes qui existe de manière redondante juste pour prendre en charge plusieurs approches équivalentes pour effectuer exactement la même tâche.

Édition supplémentaire: il existe en fait un meilleur moyen de fournir une comparaison ET un hachage pour de nombreuses classes, y compris dans la question - un __key__ méthode, comme je l'ai mentionné dans mon commentaire sur la question. Comme je n'ai jamais réussi à écrire le PEP pour cela, vous devez actuellement l'implémenter avec un Mixin (& c) si vous l'aimez:

class KeyedMixin:
  def __lt__(self, other):
    return self.__key__() < other.__key__()
  # and so on for other comparators, as above, plus:
  def __hash__(self):
    return hash(self.__key__())

Il est très courant que les comparaisons d'une instance avec d'autres instances se résument à la comparaison d'un Tuple pour chacune avec quelques champs - et ensuite, le hachage doit être implémenté exactement de la même manière. Le __key__ adresses de méthodes spéciales qui nécessitent directement.

87
Alex Martelli

Pour simplifier ce cas, il y a un décorateur de classe dans Python 2.7 +/3.2 +, functools.total_ordering , qui peut être utilisé pour implémenter ce que suggère Alex. Exemple tiré de la documentation :

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))
45
jmagnusson

Ceci est couvert par PEP 207 - Comparaisons riches

Également, __cmp__ disparaît dans python 3.0. (Notez qu'il n'est pas présent sur http://docs.python.org/3.0/reference/datamodel.html mais it IS on http://docs.python.org/2.7/reference/datamodel.html )

9
Christopher

(Modifié le 17/06/17 pour tenir compte des commentaires.)

J'ai essayé la réponse mixin comparable ci-dessus. J'ai eu des ennuis avec "Aucun". Voici une version modifiée qui gère les comparaisons d'égalité avec "Aucune". (Je n'ai vu aucune raison de s'embêter avec des comparaisons d'inégalité avec None comme manquant de sémantique):


class ComparableMixin(object):

    def __eq__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other and not other<self

    def __ne__(self, other):
        return not __eq__(self, other)

    def __gt__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return other<self

    def __ge__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other

    def __le__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not other<self    
0
Gabriel Ferrer

Inspiré par les réponses ComparableMixin & KeyedMixin d'Alex Martelli, j'ai trouvé le mixin suivant. Il vous permet d'implémenter une seule méthode _compare_to(), qui utilise des comparaisons basées sur des clés similaires à KeyedMixin, mais permet à votre classe de choisir la clé de comparaison la plus efficace en fonction du type de other. (Notez que ce mixage n'aide pas beaucoup pour les objets dont on peut tester l'égalité mais pas l'ordre).

class ComparableMixin(object):
    """mixin which implements rich comparison operators in terms of a single _compare_to() helper"""

    def _compare_to(self, other):
        """return keys to compare self to other.

        if self and other are comparable, this function 
        should return ``(self key, other key)``.
        if they aren't, it should return ``None`` instead.
        """
        raise NotImplementedError("_compare_to() must be implemented by subclass")

    def __eq__(self, other):
        keys = self._compare_to(other)
        return keys[0] == keys[1] if keys else NotImplemented

    def __ne__(self, other):
        return not self == other

    def __lt__(self, other):
        keys = self._compare_to(other)
        return keys[0] < keys[1] if keys else NotImplemented

    def __le__(self, other):
        keys = self._compare_to(other)
        return keys[0] <= keys[1] if keys else NotImplemented

    def __gt__(self, other):
        keys = self._compare_to(other)
        return keys[0] > keys[1] if keys else NotImplemented

    def __ge__(self, other):
        keys = self._compare_to(other)
        return keys[0] >= keys[1] if keys else NotImplemented
0
Eli Collins