Elegante manieren om gelijkwaardigheid (“gelijkheid”) in Python-klassen te ondersteunen

Bij het schrijven van aangepaste klassen is het vaak belangrijk om gelijkwaardigheid toe te staan ​​door middel van de operators ==en !=. In Python wordt dit mogelijk gemaakt door respectievelijk de speciale methoden __eq__en __ne__te implementeren. De gemakkelijkste manier die ik heb gevonden om dit te doen, is de volgende methode:

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)

Kent u een elegantere manier om dit te doen? Kent u bepaalde nadelen van het gebruik van de bovenstaande methode voor het vergelijken van __dict__s?

Opmerking: een beetje verduidelijking: als __eq__en __ne__niet gedefinieerd zijn, ziet u dit gedrag:

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

Dat wil zeggen, a == bevalueert tot Falseomdat het echt a is buitvoert, een identiteitstest (dwz ” Is ahetzelfde object als B?”).

Als __eq__en __ne__zijn gedefinieerd, zie je dit gedrag (dat is degene die we zoeken):

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

Antwoord 1, autoriteit 100%

Beschouw dit eenvoudige probleem:

class Number:
    def __init__(self, number):
        self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops

Dus, Python gebruikt standaard de object-ID’s voor vergelijkingsbewerkingen:

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

Het negeren van de functie __eq__lijkt het probleem op te lossen:

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

Onthoud in Python 2altijd dat u ook de functie __ne__overschrijft, evenals de documentatiestelt:

Er zijn geen impliciete relaties tussen de vergelijkingsoperatoren. De
waarheid van x==yimpliceert niet dat x!=yonwaar is. Dienovereenkomstig, wanneer?
het definiëren van __eq__(), moet men ook __ne__()definiëren zodat de
operators zullen zich gedragen zoals verwacht.

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)
n1 == n2 # True
n1 != n2 # False

In Python 3is dit niet langer nodig, omdat de documentatiestelt:

Standaard delegeert __ne__()naar __eq__()en keert het resultaat om
tenzij het NotImplementedis. Er zijn geen andere geïmpliceerde
relaties tussen de vergelijkingsoperatoren, bijvoorbeeld de waarheid
van (x<y or x==y)betekent niet x<=y.

Maar dat lost niet al onze problemen op. Laten we een subklasse toevoegen:

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

Opmerking:Python 2 heeft twee soorten klassen:

  • klassiek- style(of oude stijl) klassen, die nieterven van objecten die worden gedeclareerd als class A:, class A():of class A(B):waarbij Been klassieke stijl is klas;

  • nieuw- styleklassen, die erven van objecten die worden gedeclareerd als class A(object)of class A(B):waarbij Been klasse nieuwe stijl is. Python 3 heeft alleen klassen nieuwe stijl die zijn gedeclareerd als class A:, class A(object):of class A(B):.

Voor klassen in klassieke stijl roept een vergelijkingsbewerking altijd de methode van de eerste operand aan, terwijl voor klassen nieuwe stijl altijd de methode van de operand van de subklasse wordt aangeroepen, ongeacht de volgorde van de operanden.

Dus hier, als Numbereen klas in klassieke stijl is:

  • n1 == n3roept n1.__eq__aan;
  • n3 == n1roept n3.__eq__aan;
  • n1 != n3roept n1.__ne__aan;
  • n3 != n1roept n3.__ne__aan.

En als Numbereen klasse nieuwe stijl is:

  • zowel n1 == n3als n3 == n1bellen n3.__eq__;
  • zowel n1 != n3als n3 != n1bellen n3.__ne__.

Om het niet-commutativiteitsprobleem van de operators ==en !=voor de klassieke klassen van Python 2 op te lossen, worden de __eq__en __ne__-methoden moeten de waarde NotImplementedretourneren als een operandtype niet wordt ondersteund. De documentatiedefinieert de NotImplementedwaarde als:

Numerieke methoden en uitgebreide vergelijkingsmethoden kunnen deze waarde retourneren als
ze implementeren de bewerking niet voor de opgegeven operanden. (De
interpreter zal dan de gereflecteerde bewerking proberen, of een andere
fallback, afhankelijk van de operator.) De waarheidswaarde is waar.

In dit geval delegeert de operator de vergelijkingsbewerking aan de gereflecteerde methodevan de andereoperand. De documentatiedefinieert gereflecteerde methoden als:

Er zijn geen versies met verwisselde argumenten van deze methoden (te gebruiken)
wanneer het linkerargument de bewerking niet ondersteunt, maar het rechterargument
argument doet); in plaats daarvan zijn __lt__()en __gt__()elkaars
reflectie, __le__()en __ge__()zijn elkaars reflectie, en
__eq__()en __ne__()zijn hun eigen reflectie.

Het resultaat ziet er als volgt uit:

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 NotImplemented:
        return NotImplemented
    return not x

Het retourneren van de waarde NotImplementedin plaats van Falseis de juiste keuze, zelfs voor klassen nieuwe stijl als commutativiteitvan de ==en !=operators is gewenst wanneer de operanden van niet-gerelateerde typen zijn (geen overerving).

Zijn we er al? Niet helemaal. Hoeveel unieke nummers hebben we?

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

Sets gebruiken de hashes van objecten, en standaard retourneert Python de hash van de identifier van het object. Laten we proberen het te negeren:

def __hash__(self):
    """Overrides the default implementation"""
    return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1

Het eindresultaat ziet er als volgt uit (ik heb aan het eind enkele beweringen toegevoegd ter validatie):

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

Antwoord 2, autoriteit 54%

Je moet voorzichtig zijn met overerving:

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

Controleer typen strenger, zoals deze:

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

Bovendien zal je aanpak prima werken, daar zijn speciale methoden voor.


Antwoord 3, autoriteit 39%

De manier die je beschrijft, is de manier waarop ik het altijd heb gedaan. Omdat het volledig generiek is, kun je die functionaliteit altijd opsplitsen in een mixin-klasse en deze overnemen in klassen waar je die functionaliteit wilt.

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

Antwoord 4, autoriteit 4%

Geen direct antwoord, maar leek relevant genoeg om aan te pakken, omdat het af en toe een beetje uitgebreide verveling bespaart. Rechtstreeks uit de documenten knippen…


functools.total_ordering(cls)

Gegeven een klasse die een of meer uitgebreide vergelijkingsmethodes definieert, levert deze klassedecorateur de rest.Dit vereenvoudigt de inspanning die nodig is om alle mogelijke uitgebreide vergelijkingsbewerkingen te specificeren:

De klasse moet een van __lt__(), __le__(), __gt__()of __ge__(). Daarnaast moet de klasse een __eq__()methode leveren.

Nieuw in versie 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()))

Antwoord 5, autoriteit 2%

U hoeft niet zowel __eq__als __ne__te overschrijven, u kunt alleen __cmp__overschrijven, maar dit heeft gevolgen voor het resultaat van ==, !==, < , > enzovoort.

istests voor objectidentiteit. Dit betekent dat a isb Truezal zijn in het geval dat a en b beide de verwijzing naar hetzelfde object bevatten. In python heb je altijd een verwijzing naar een object in een variabele, niet het eigenlijke object, dus in wezen voor a is b om waar te zijn, moeten de objecten erin zich op dezelfde geheugenlocatie bevinden. Hoe en vooral waarom zou je dit gedrag negeren?

Bewerken: ik wist niet dat __cmp__uit python 3 was verwijderd, dus vermijd het.


Antwoord 6

Van dit antwoord: https://stackoverflow.com/a/30676267/541136heb ik aangetoond dat, terwijl het is correct om __ne__te definiëren in termen __eq__– in plaats van

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

je moet gebruiken:

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

Antwoord 7

Ik denk dat de twee termen die u zoekt gelijkheid(==) en identiteit(is) zijn. Bijvoorbeeld:

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

Antwoord 8

De ‘is’-test zal de identiteit testen met behulp van de ingebouwde ‘id()’-functie die in wezen het geheugenadres van het object retourneert en daarom niet overbelastbaar is.

In het geval van het testen van de gelijkheid van een klasse, wil je waarschijnlijk wat strenger zijn in je tests en alleen de gegevensattributen in je klasse vergelijken:

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

Deze code vergelijkt alleen niet-functionele gegevensleden van uw klasse en slaat ook alles over wat privé is wat u over het algemeen wilt. In het geval van gewone oude Python-objecten heb ik een basisklasse die __init__, __str__, __repr__ en __eq__ implementeert, zodat mijn POPO-objecten niet de last dragen van al die extra (en in de meeste gevallen identieke) logica.


Antwoord 9

In plaats van subklassen/mixins te gebruiken, gebruik ik graag een generieke klassedecorateur

def comparable(cls):
    """ Class decorator providing generic comparison functionality """
    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
    def __ne__(self, other):
        return not self.__eq__(other)
    cls.__eq__ = __eq__
    cls.__ne__ = __ne__
    return cls

Gebruik:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x
a = Number(1)
b = Number(1)
assert a == b

Antwoord 10

Dit omvat de opmerkingen over het antwoord van Algorias en vergelijkt objecten met een enkel kenmerk, omdat het hele dict me niet interesseert. hasattr(other, "id")moet waar zijn, maar ik weet dat dit komt omdat ik het in de constructor heb ingesteld.

def __eq__(self, other):
    if other is self:
        return True
    if type(other) is not type(self):
        # delegate to superclass
        return NotImplemented
    return other.id == self.id

Antwoord 11

Ik heb een aangepaste basis geschreven met een standaardimplementatie van __ne__die eenvoudig __eq__negeert:

class HasEq(object):
  """
  Mixin that provides a default implementation of ``object.__neq__`` using the subclass's implementation of ``object.__eq__``.
  This overcomes Python's deficiency of ``==`` and ``!=`` not being symmetric when overloading comparison operators
  (i.e. ``not x == y`` *does not* imply that ``x != y``), so whenever you implement
  `object.__eq__ <https://docs.python.org/2/reference/datamodel.html#object.__eq__>`_, it is expected that you
  also implement `object.__ne__ <https://docs.python.org/2/reference/datamodel.html#object.__ne__>`_
  NOTE: in Python 3+ this is no longer necessary (see https://docs.python.org/3/reference/datamodel.html#object.__ne__)
  """
  def __ne__(self, other):
    """
    Default implementation of ``object.__ne__(self, other)``, delegating to ``self.__eq__(self, other)``.
    When overriding ``object.__eq__`` in Python, one should also override ``object.__ne__`` to ensure that
    ``not x == y`` is the same as ``x != y``
    (see `object.__eq__ <https://docs.python.org/2/reference/datamodel.html#object.__eq__>`_ spec)
    :return: ``NotImplemented`` if ``self.__eq__(other)`` returns ``NotImplemented``, otherwise ``not self.__eq__(other)``
    """
    equal = self.__eq__(other)
    # the above result could be either True, False, or NotImplemented
    if equal is NotImplemented:
      return NotImplemented
    return not equal

Als u van deze basisklasse erft, hoeft u alleen __eq__en de basis te implementeren.

Achteraf gezien was het misschien beter geweest om het in plaats daarvan als decorateur te implementeren. Iets als @functools.total_ordering

Other episodes