Eleganckie sposoby wspierania równoważności ("equality") w klasach Pythona

Podczas pisania klas niestandardowych często ważne jest, aby zezwolić na równoważność za pomocą operatorów == i !=. W Pythonie jest to możliwe dzięki implementacji, odpowiednio, specjalnych metod __eq__ i __ne__. Najprostszym sposobem, jaki znalazłem, jest następująca metoda:

class Foo:
    def __init__(self, item):
        self.item = item

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

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

Czy znasz bardziej eleganckie sposoby, aby to zrobić? Czy znasz jakieś szczególne wady stosowania powyższej metody porównywania __dict__ s?

Uwaga : trochę Wyjaśnienie--gdy __eq__ i __ne__ są niezdefiniowane, znajdziesz takie zachowanie:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

Czyli a == b ocenia na False, ponieważ naprawdę działa a is b, test tożsamości (tj. "czy a jest tym samym obiektem co b?").

Kiedy __eq__ i __ne__ są zdefiniowane, znajdziesz takie zachowanie (którego szukamy):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
Author: gotgenes, 2008-12-24

8 answers

Rozważ ten prosty problem:

class Number:

    def __init__(self, number):
        self.number = number


n1 = Number(1)
n2 = Number(1)

n1 == n2 # False -- oops

Tak więc Python domyślnie używa identyfikatorów obiektów do operacji porównywania:

id(n1) # 140400634555856
id(n2) # 140400634555920

Nadpisanie funkcji __eq__ wydaje się rozwiązywać problem:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return False


n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3

W Python 2 zawsze pamiętaj, aby nadpisać funkcję __ne__, ponieważ dokumentacja stwierdza:

Między operatorami porównania nie istnieją żadne implikowane relacje. Na prawda x==y nie oznacza, że x!=y jest fałsz. Odpowiednio, gdy definiowanie __eq__(), należy również zdefiniować __ne__(), aby operatorzy będą zachowywać się zgodnie z oczekiwaniami.

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)


n1 == n2 # True
n1 != n2 # False

W Python 3 , nie jest to już konieczne, ponieważ dokumentacja stwierdza:

Domyślnie, __ne__() deleguje na __eq__() i odwraca wynik chyba że jest NotImplemented. Nie ma innych domniemanych zależności między operatorami porównania, np. prawda z (x<y or x==y) nie oznacza x<=y.

Ale to nie rozwiązuje wszystkich naszych problemów. Dodajmy podklasę:
class SubNumber(Number):
    pass


n3 = SubNumber(1)

n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False

Uwaga: Python 2 ma dwa rodzaje klas:

  • Styl Klasyczny (lub W starym stylu) klasy, które nie dziedziczą od object i które są zadeklarowane jako class A:, class A(): lub class A(B): gdzie B jest klasą w stylu klasycznym;

  • nowy-styl klasy, które dziedziczą z object i które są zadeklarowane jako class A(object) lub class A(B):, gdzie B jest klasą nowego stylu. Python 3 ma tylko klasy nowego stylu, które są zadeklarowane jako class A:, class A(object): lub class A(B):.

Dla klasy klasy, operacja porównawcza zawsze wywołuje metodę pierwszego operandu, podczas gdy dla klasy nowego stylu, zawsze wywołuje metodę podklasy operandu, niezależnie od kolejności operandów .

Więc tutaj, jeśli Number jest stylem klasycznym Klasa:

  • n1 == n3 wywołania n1.__eq__;
  • n3 == n1 wywołania n3.__eq__;
  • n1 != n3 wywołania n1.__ne__;
  • n3 != n1 połączenia n3.__ne__.

I jeśli Number jest klasą nowego stylu:

  • zarówno n1 == n3 jak i n3 == n1 call n3.__eq__;
  • zarówno n1 != n3 jak i n3 != n1 zadzwoń n3.__ne__.

Aby rozwiązać problem nieprzemienności operatorów == i != dla klasy 2 w Pythonie, metody __eq__ i __ne__ powinny zwracać NotImplemented wartość, gdy typ operandu nie jest obsługiwany. Dokumentacja definiuje wartość NotImplemented jako:

Metody numeryczne i metody porównawcze rich mogą zwracać tę wartość, jeśli nie realizują operacji dla podanych operandów. (The interpreter spróbuje wtedy operacji odbicia, lub innej w zależności od operatora.) Jego wartość prawdy jest prawdziwa.

W tym przypadku operator przekazuje operację porównania do metoda odbicia zinne operand. Dokumentacja definiuje metody refleksyjne jako:

Nie ma wersji podmienionych argumentów tych metod (do użycia gdy lewy argument nie wspiera operacji, ale prawy argument robi); raczej __lt__() i __gt__() są wzajemnie refleksji, __le__() i __ge__() są wzajemnie odbiciem i __eq__() i __ne__() są ich własnym odbiciem.

Wynik wygląda następująco to:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return NotImplemented

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    x = self.__eq__(other)
    if x is not NotImplemented:
        return not x
    return NotImplemented

Zwracanie wartości NotImplemented zamiast {[61] } jest właściwe nawet dla klas nowego stylu, jeślikomutatywność operatorów == i != jest pożądana, gdy operandy są niepowiązanych typów (bez dziedziczenia).

Jesteśmy już na miejscu? Niezupełnie. Ile mamy unikalnych numerów?

len(set([n1, n2, n3])) # 3 -- oops

Sets używa skrótów obiektów i domyślnie Python zwraca hash identyfikatora obiektu. Spróbujmy obejść it:

def __hash__(self):
    """Overrides the default implementation"""
    return hash(tuple(sorted(self.__dict__.items())))

len(set([n1, n2, n3])) # 1

Wynik końcowy wygląda tak (dodałem kilka twierdzeń na końcu dla walidacji):

class Number:

    def __init__(self, number):
        self.number = number

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Number):
            return self.number == other.number
        return NotImplemented

    def __ne__(self, other):
        """Overrides the default implementation (unnecessary in Python 3)"""
        x = self.__eq__(other)
        if x is not NotImplemented:
            return not x
        return NotImplemented

    def __hash__(self):
        """Overrides the default implementation"""
        return hash(tuple(sorted(self.__dict__.items())))


class SubNumber(Number):
    pass


n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)

assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1

assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1

assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1

assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2
 206
Author: Tal Weiss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-10 21:56:28

Musisz uważać na dziedziczenie:

>>> class Foo:
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

>>> class Bar(Foo):pass

>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False

Sprawdzaj typy ściślej, jak to:

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

Poza tym, twoje podejście będzie działać dobrze, po to są specjalne metody.

 182
Author: Algorias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-24 02:30:13

Sposób, w jaki opisujesz, to sposób, w jaki zawsze to robiłem. Ponieważ jest całkowicie ogólna, zawsze możesz podzielić tę funkcjonalność na klasę mixin i dziedziczyć ją w klasach, w których chcesz tę funkcjonalność.

class CommonEqualityMixin(object):

    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)

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

class Foo(CommonEqualityMixin):

    def __init__(self, item):
        self.item = item
 154
Author: cdleary,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-07 22:01:02

Nie jest to bezpośrednia odpowiedź, ale wydaje się na tyle istotna, że można ją wykorzystać, ponieważ oszczędza to trochę gadatliwej nudy przy okazji. Prosto od lekarzy...


Functools.total_ordering (cls)

Biorąc pod uwagę klasę definiującą jedną lub więcej bogatych metod zamawiania porównań, ta klasa dostarcza resztę. upraszcza to wysiłek związany z określeniem wszystkich możliwych bogatych operacji porównawczych:

Klasa musi zdefiniować jedną z lt (), le(), gt () lub ge(). Ponadto klasa powinna dostarczyć metodę eq ().

Nowość w wersji 2.7

@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()))
 13
Author: John Mee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-20 00:34:26

Nie musisz nadpisywać zarówno __eq__ jak i __ne__ możesz nadpisać tylko __cmp__ ale to spowoduje implikację na wyniku==,!= = , i tak dalej.

is testy tożsamości obiektu. Oznacza to, że a is b będzie True W przypadku, gdy a i b zawierają odniesienie do tego samego obiektu. W Pythonie zawsze przechowujesz odniesienie do obiektu w zmiennej, a nie do rzeczywistego obiektu, więc zasadniczo, Aby a było prawdą, obiekty w nich powinny znajdować się w tej samej pamięci miejsce. W jaki sposób i co najważniejsze, dlaczego miałby Pan to ignorować?

Edit: nie wiedziałem, że __cmp__ został usunięty z Pythona 3, więc go unikaj.

 8
Author: Vasil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-03 06:14:44

Od tej odpowiedzi: https://stackoverflow.com/a/30676267/541136 udowodniłem, że chociaż poprawne jest definiowanie __ne__ w kategoriach __eq__ - zamiast

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

Powinieneś użyć:

def __ne__(self, other):
    return not self == other
 3
Author: Aaron Hall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:29

Myślę, że dwa terminy, których szukasz to equality ( = = ) i identity (is). Na przykład:

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True       <-- a and b have values which are equal
>>> a is b
False      <-- a and b are not the same list object
 2
Author: too much php,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-23 23:12:07

Test 'is' sprawdzi tożsamość przy użyciu wbudowanej funkcji ' id ()', która zasadniczo zwraca adres pamięci obiektu i dlatego nie jest przeciążalna.

Jednak w przypadku testowania równości klasy prawdopodobnie chcesz być trochę bardziej rygorystyczny w swoich testach i porównywać tylko atrybuty danych w swojej klasie:

import types

class ComparesNicely(object):

    def __eq__(self, other):
        for key, value in self.__dict__.iteritems():
            if (isinstance(value, types.FunctionType) or 
                    key.startswith("__")):
                continue

            if key not in other.__dict__:
                return False

            if other.__dict__[key] != value:
                return False

         return True

Ten kod porównuje tylko niefunkcyjne dane członków twojej klasy, a także pomija wszystko prywatne, co jest ogólnie czego chcesz. W przypadku zwykłych starych obiektów Pythona mam klasę bazową, która implementuje _ _ INIT__, _ _ str__,__ repr _ _ i _ _ eq__, więc moje obiekty POPO nie przenoszą ciężaru całej tej dodatkowej (i w większości przypadków identycznej) logiki.

 1
Author: mcrute,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-12-24 03:00:53