__lt__ au lieu de __cmp__

100

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__. On dit que les surcharges de comparaison riches sont 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 en implémenter plusieurs avec une logique presque identique. Cependant, si vous pouvez utiliser l'ordre intégré cmpet le tri des tuples , cela __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 permuté» / comportement reflété, mais cela entraîne une augmentation nette de la complication, à mon humble avis.)

Y a-t-il des écueils imprévus dont je dois être informé si je ne fais que surcharger __cmp__?

Je comprends la <, <=, ==, etc. opérateurs peuvent être surchargées à d' autres fins, et peut retourner tout objet qu'ils aiment. Je ne m'interroge pas 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 l'a souligné Christopher , cmpdisparaît dans 3.x. Existe-t-il des alternatives qui facilitent la mise en œuvre des comparaisons __cmp__?

Communauté
la source
5
Voir ma réponse à votre dernière question, mais en fait, il existe une conception qui rendrait les choses encore plus faciles pour de nombreuses classes, y compris la vôtre (pour le moment, vous avez besoin d'un mixin, d'une métaclasse ou d'un décorateur de classe pour l'appliquer): si une méthode spéciale clé est présente, il doit retourner un tuple de valeurs, et tous les comparateurs ET le hachage sont définis en fonction de ce tuple. Guido a aimé mon idée quand je lui ai expliqué, mais ensuite je me suis occupé d'autres choses et je n'ai jamais eu le temps d'écrire un PEP ... peut-être pour 3.2 ;-). En attendant, je continue d'utiliser mon mixin pour ça! -)
Alex Martelli

Réponses:

90

Oui, il est facile de tout implémenter, 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 juste __lt__et multiplier l'héritage de ComparableMixin (après toutes les autres bases dont elle 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écore (le résultat pourrait être microscopiquement plus rapide à l'exécution, à un coût tout aussi infime en termes de mémoire).

Bien sûr, si votre classe a un moyen particulièrement rapide de mettre en œuvre (par exemple) __eq__et __ne__, elle doit les définir directement afin 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 que comme:

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__fallait aller, puisque nous fait avoir __lt__et les amis, pourquoi garder une autre, autre façon de faire exactement la même chose autour? C'est tellement de poids mort dans chaque runtime Python (Classic, Jython, IronPython, PyPy, ...). Le code qui n'aura certainement pas de bogues est le code qui n'existe pas - d'où le principe de Python selon lequel il devrait idéalement y avoir 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 sortons de notre façon d'interdire les choses (par exemple, quasi-équivalence entre mixins et décorateurs de classe pour certains usages), mais il a vraiment fait dire que nous ne sommes pas comme à transporter le code dans les compilateurs et / ou des environnements d'exécution qui existent de manière redondante juste pour prendre en charge plusieurs approches équivalentes pour effectuer exactement la même tâche.

Modification supplémentaire: il existe en fait un moyen encore meilleur de fournir une comparaison ET un hachage pour de nombreuses classes, y compris cela dans la question - une __key__méthode, comme je l'ai mentionné dans mon commentaire à la question. Comme je n'ai jamais eu le temps d'é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 à comparer un tuple pour chacune avec quelques champs - et ensuite, le hachage doit être implémenté exactement sur la même base. La __key__méthode spéciale répond directement aux besoins.

Alex Martelli
la source
Désolé pour le retard @R. Pate, j'ai décidé que puisque je devais modifier de toute façon, je devrais fournir la réponse la plus complète que je pourrais plutôt que de me précipiter (et je viens de modifier à nouveau pour suggérer mon ancienne idée clé que je n'ai jamais eue à PEPping, ainsi que comment pour l'implémenter avec un mixin).
Alex Martelli
J'aime vraiment cette idée clé , je vais l'utiliser et voir ce que ça fait. (Bien que nommé cmp_key ou _cmp_key au lieu d'un nom réservé.)
TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixinquand j'essaye ceci en Python 3. Voir le code complet sur gist.github.com/2696496
Adam Parkin
2
En Python 2.7 + / 3.2 +, vous pouvez utiliser functools.total_orderingau lieu de créer le vôtre ComparableMixim. Comme suggéré dans la réponse de jmagnusson
Jour
4
Utiliser <pour implémenter __eq__dans Python 3 est une très mauvaise idée, à cause de TypeError: unorderable types.
Antti Haapala
49

Pour simplifier ce cas, il existe un décorateur de classe en Python 2.7 + / 3.2 +, functools.total_ordering , qui peut être utilisé pour implémenter ce qu'Alex suggère. 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()))
jmagnusson
la source
9
total_orderingne met pas en œuvre __ne__cependant, alors attention!
Flimm
3
@Flimm, ce n'est pas le cas, mais __ne__. mais c'est parce __ne__que l'implémentation par défaut est déléguée à __eq__. Il n'y a donc rien à surveiller ici.
Jan Hudec
doit définir au moins une opération de commande: <> <=> = .... eq n'est pas nécessaire comme commande totale si! a <b et b <a puis a = b
Xanlantos
9

Ceci est couvert par la PEP 207 - Comparaisons riches

De plus, __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 il EST sur http://docs.python.org/2.7/reference/datamodel.html )

Christophe
la source
Le PEP ne s'intéresse qu'aux raisons pour lesquelles des comparaisons riches sont nécessaires, de la manière dont les utilisateurs de NumPy veulent que A <B renvoie une séquence.
Je n'avais pas réalisé que ça s'en allait définitivement, cela me rend triste. (Mais merci de l'avoir signalé.)
Le PEP discute également "pourquoi" ils sont préférés. Essentiellement, cela se résume à l'efficacité: 1. Pas besoin d'implémenter des opérations qui n'ont aucun sens pour votre objet (comme des collections non ordonnées.) 2. Certaines collections ont des opérations très efficaces sur certains types de comparaisons. Les comparaisons riches permettent à l'interprète de profiter de cela si vous les définissez.
Christopher
1
Re 1, s'ils n'ont pas de sens, n'implémentez pas cmp . Re 2, avoir les deux options peut vous permettre d'optimiser au besoin, tout en réalisant un prototypage et des tests rapides. Aucun des deux ne me dit pourquoi il a été supprimé. (Essentiellement, cela se résume à l'efficacité du développeur pour moi.) Est-il possible que les comparaisons riches soient moins efficaces avec le repli cmp en place? Cela n'aurait aucun sens pour moi.
1
@R. Pate, comme j'essaie de l'expliquer dans ma réponse, il n'y a pas de vraie perte de généralité (puisqu'un mixin, un décorateur ou une métaclasse, vous permet de tout définir facilement en termes de juste <si vous le souhaitez) et donc pour toutes les implémentations Python à transporter le code redondant retombant pour toujours à cmp - juste pour permettre aux utilisateurs de Python d'exprimer les choses de deux manières équivalentes - fonctionnerait à 100% contre le grain de Python.
Alex Martelli
2

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

J'ai essayé la réponse mixin comparable ci-dessus. J'ai rencontré des problèmes avec "Aucun". Voici une version modifiée qui gère les comparaisons d'égalité avec "Aucun". (Je ne voyais aucune raison de s'embêter avec les comparaisons d'inégalités 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    
Gabriel Ferrer
la source
Comment pensez-vous que selfpourrait être le singleton Nonede NoneTypeet en même temps mettre en œuvre votre ComparableMixin? Et en effet cette recette est mauvaise pour Python 3.
Antti Haapala
3
selfsera jamais être None, de sorte que la branche peut aller tout. N'utilisez pas type(other) == type(None); utilisez simplement other is None. Plutôt que de -boîtier spécial None, test si l'autre type est une instance du type de selfet retourner le NotImplementedsingleton sinon: if not isinstance(other, type(self)): return NotImplemented. Faites ceci pour toutes les méthodes. Python pourra alors donner à l'autre opérande une chance de fournir une réponse à la place.
Martijn Pieters
1

Inspiré par les ComparableMixin& KeyedMixinréponses d'Alex Martelli , je suis venu avec le mixin suivant. Il vous permet d'implémenter une _compare_to()méthode unique , 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 mixin n'aide pas beaucoup pour les objets qui peuvent être testés pour l'égalité mais pas pour 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
Eli Collins
la source