__lt__ zamiast __cmp__

100

Python 2.x ma dwa sposoby na przeciążenie operatorów porównania __cmp__lub „bogatych operatorów porównania”, takich jak __lt__. Mówi się, że preferowane są bogate przeciążenia porównawcze, ale dlaczego tak jest?

Bogate operatory porównania są prostsze w implementacji każdego z nich, ale musisz zaimplementować kilka z nich z prawie identyczną logiką. Jeśli jednak możesz użyć cmpporządkowania wbudowanego i krotki, to __cmp__robi się całkiem proste i spełnia wszystkie porównania:

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

Ta prostota wydaje się znacznie lepiej spełniać moje potrzeby niż przeładowanie wszystkich 6 (!) Bogatych porównań. (Jednak możesz sprowadzić to do „tylko” 4, jeśli polegasz na „zamienionym argumencie” / odzwierciedlonym zachowaniu, ale moim skromnym zdaniem powoduje to wzrost netto komplikacji).

Czy są jakieś nieprzewidziane pułapki, o których muszę wiedzieć, jeśli tylko przeciążę __cmp__?

Rozumiem <, <=, ==itp operatorzy mogą być przeciążone dla innych celów, a każdy obiekt może powrócić im się podoba. Nie pytam o zalety tego podejścia, a tylko o różnice, gdy używam tych operatorów do porównań w tym samym sensie, co dla liczb.

Aktualizacja: Jak zauważył Christopher , cmpznika w 3.x. Czy są jakieś alternatywy, które sprawiają, że wykonywanie porównań jest tak proste, jak powyższe __cmp__?

Społeczność
źródło
5
Zobacz moją odpowiedź na twoje ostatnie pytanie, ale w rzeczywistości istnieje projekt, który jeszcze bardziej ułatwiłby pracę wielu klasom, w tym twoim (teraz potrzebujesz miksera, metaklasy lub dekoratora klas, aby go zastosować): jeśli obecna jest kluczowa metoda specjalna, musi zwrócić krotkę wartości, a wszystkie komparatory AND hash są zdefiniowane w odniesieniu do tej krotki. Guido spodobał się mój pomysł, kiedy mu to wyjaśniłem, ale potem zająłem się innymi sprawami i nigdy nie zacząłem pisać PEP ... może za 3,2 ;-). W międzyczasie nadal używam do tego mojego miksu! -)
Alex Martelli

Odpowiedzi:

90

Tak, łatwo jest zaimplementować wszystko, np. __lt__Z klasą mixin (lub metaklasą, lub dekoratorem klas, jeśli tak się dzieje).

Na przykład:

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

Teraz twoja klasa może zdefiniować po prostu __lt__i pomnożyć dziedziczenie z ComparableMixin (po wszelkich innych podstawach, których potrzebuje). Dekorator klas byłby dość podobny, po prostu wstawiałby podobne funkcje jako atrybuty nowej klasy, którą dekoruje (wynik może być mikroskopijnie szybszy w czasie wykonywania, przy równie niewielkim koszcie pamięci).

Oczywiście, jeśli twoja klasa ma jakiś szczególnie szybki sposób na zaimplementowanie (np.) __eq__I __ne__powinna zdefiniować je bezpośrednio, aby wersje miksera nie były używane (na przykład, tak jest dict) - w rzeczywistości __ne__można to zdefiniować, aby ułatwić tak jak:

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

ale w powyższym kodzie chciałem zachować przyjemną symetrię tylko używania <;-). Dlaczego __cmp__musiałeś odejść, ponieważ nie mają __lt__i przyjaciele, dlaczego zachować kolejny, inny sposób to zrobić dokładnie to samo dookoła? To po prostu tak dużo martwej wagi w każdym środowisku uruchomieniowym Pythona (Classic, Jython, IronPython, PyPy, ...). Kod na pewno nie będzie zawierał błędów, to kod, którego nie ma - skąd zasada Pythona, że ​​powinien istnieć jeden oczywisty sposób wykonania zadania (C ma tę samą zasadę w sekcji „Spirit of C” w przy okazji standard ISO).

To nie znaczy, wychodzimy z naszej drodze do zakazania rzeczy (np bliskiej równoważność wstawek i klasa dekoratorów dla niektórych zastosowań), ale na pewno nie oznacza, że nie lubią nosić kod w kompilatorów i / lub środowiska wykonawcze, które istnieją nadmiarowo tylko po to, aby obsługiwać wiele równoważnych podejść do wykonania dokładnie tego samego zadania.

Dalsza edycja: w rzeczywistości istnieje jeszcze lepszy sposób na zapewnienie porównania ORAZ haszowanie dla wielu klas, w tym w pytaniu - __key__metoda, jak wspomniałem w komentarzu do pytania. Ponieważ nigdy nie udało mi się napisać dla niego PEP, musisz obecnie zaimplementować go z Mixinem (& c), jeśli chcesz:

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__())

Bardzo częstym przypadkiem jest, gdy porównania instancji z innymi instancjami sprowadzają się do porównania krotki dla każdego z kilkoma polami - a następnie haszowanie powinno być realizowane na dokładnie tej samej podstawie. Te __key__specjalne adresy metody, które muszą bezpośrednio.

Alex Martelli
źródło
Przepraszamy za opóźnienie @R. Pate, zdecydowałem, że skoro i tak muszę edytować, powinienem udzielić najdokładniejszej odpowiedzi, jaką mogę, zamiast spieszyć się (i właśnie zredagowałem ponownie, aby zasugerować mój stary kluczowy pomysł, którego nigdy nie przeszedłem do PEPping), a także jak zaimplementować go z mixinem).
Alex Martelli
Naprawdę podoba mi się ten kluczowy pomysł, zamierzam go wykorzystać i zobaczyć, jakie to uczucie. (Chociaż nazwano cmp_key lub _cmp_key zamiast zarezerwowanej nazwy.)
TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixinkiedy spróbuję w Pythonie 3. Zobacz pełny kod na gist.github.com/2696496
Adam Parkin
2
W Pythonie 2.7 + / 3.2 + możesz używać functools.total_orderingzamiast tworzyć własne ComparableMixim. Jak zasugerowano w odpowiedzi jmagnusson
dzień
4
Używanie <do implementacji __eq__w Pythonie 3 jest dość złym pomysłem z powodu TypeError: unorderable types.
Antti Haapala,
49

Aby uprościć ten przypadek, istnieje dekorator klas w Pythonie 2.7 + / 3.2 +, functools.total_ordering , który może być użyty do zaimplementowania tego, co sugeruje Alex. Przykład z dokumentów:

@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
źródło
9
total_orderingjednak nie wdraża __ne__, więc uważaj!
Flimm
3
@Flimm, nie, ale __ne__. ale to dlatego, że __ne__ma domyślną implementację, która jest delegowana __eq__. Więc nie ma tu na co uważać.
Jan Hudec
musi zdefiniować co najmniej jedną operację zamawiania: <> <=> = .... eq nie jest potrzebne jako całkowite zamówienie, jeśli! a <b i b <a następnie a = b
Xanlantos
9

Jest to objęte PEP 207 - Rich Comparisons

Ponadto __cmp__odchodzi w Pythonie 3.0. (Zauważ, że nie ma go na http://docs.python.org/3.0/reference/datamodel.html, ale jest na http://docs.python.org/2.7/reference/datamodel.html )

Krzysztof
źródło
PEP zajmuje się tylko tym, dlaczego potrzebne są bogate porównania, w sposób, w jaki użytkownicy NumPy chcą, aby A <B zwracało sekwencję.
Nie zdawałem sobie sprawy, że to zdecydowanie odejdzie, to mnie zasmuca. (Ale dzięki za zwrócenie uwagi.)
PEP omawia również „dlaczego” są preferowane. Zasadniczo sprowadza się to do wydajności: 1. Nie ma potrzeby implementowania operacji, które nie mają sensu dla twojego obiektu (jak nieuporządkowane kolekcje). 2. Niektóre kolekcje mają bardzo wydajne operacje na niektórych rodzajach porównań. Bogate porównania pozwalają tłumaczowi to wykorzystać, jeśli je zdefiniujesz.
Christopher
1
Ad 1, Jeśli nie mają sensu, nie wdrażaj cmp . Re 2, mając obie opcje, możesz optymalizować w razie potrzeby, jednocześnie szybko prototypując i testując. Żaden z nich nie mówi mi, dlaczego został usunięty. (Zasadniczo sprowadza się to dla mnie do wydajności programisty). Czy to możliwe, że bogate porównania są mniej wydajne, gdy zastosowana jest funkcja zastępcza cmp ? To nie miałoby dla mnie sensu.
1
@R. Pate, jak staram się wyjaśnić w mojej odpowiedzi, nie ma prawdziwej straty w ogólności (ponieważ miksowanie, dekorator lub metaklasa pozwala łatwo zdefiniować wszystko w kategoriach tylko <, jeśli chcesz), a więc wszystkie implementacje Pythona do noszenia nadmiarowy kod wracający na zawsze do cmp - tylko po to, aby użytkownicy Pythona mogli wyrażać rzeczy na dwa równoważne sposoby - działałby w 100% pod prąd Pythona.
Alex Martelli
2

(Edytowano 17.06.2017 w celu uwzględnienia komentarzy).

Wypróbowałem podobną odpowiedź mixin powyżej. Wpadłem w kłopoty z „Brak”. Oto zmodyfikowana wersja, która obsługuje porównania równości z wartością „Brak”. (Nie widziałem powodu, by zawracać sobie głowę porównaniami nierówności z None jako pozbawionym semantyki):


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
źródło
Jak sądzisz, że selfmoże być Singleton Noneof NoneTypea jednocześnie realizować swoje ComparableMixin? I rzeczywiście, ten przepis jest zły dla Pythona 3.
Antti Haapala
3
selfbędzie nigdy być Nonetak, że oddział może przejść w całości. Nie używaj type(other) == type(None); po prostu użyj other is None. Zamiast specjalnego-obudowy None, sprawdzić, czy inny typ jest przykładem typu selfi zawrócić NotImplementedpojedyncza jeśli nie: if not isinstance(other, type(self)): return NotImplemented. Zrób to dla wszystkich metod. Python może wtedy dać drugiemu operandowi szansę na podanie odpowiedzi.
Martijn Pieters
1

Zainspirowany ComparableMixini KeyedMixinodpowiedziami Alexa Martellego wymyśliłem następujący mixin. Umożliwia zaimplementowanie pojedynczej _compare_to()metody, która używa porównań opartych na kluczach podobnych do KeyedMixin, ale umożliwia Twojej klasie wybranie najbardziej wydajnego klucza porównania na podstawie typu other. (Zauważ, że ta mieszanka nie pomaga zbytnio w przypadku obiektów, które można przetestować pod kątem równości, ale nie porządku).

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
źródło