Элегантные способы поддержки эквивалентности ( "равенства" ) в классах Python

При написании пользовательских классов часто бывает важно разрешить эквивалентность с помощью операторов == и !=. В Python это стало возможным благодаря реализации специальных методов __eq__ и __ne__. Самый простой способ, который я нашел для этого, - это следующий метод:

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)

Знаете ли вы более элегантные средства для этого? Знаете ли вы какие-либо особые недостатки в использовании вышеуказанного метода сравнения __dict__ s?

Примечание: немного уточнения - когда __eq__ и __ne__ являются undefined, вы найдете это поведение:

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

То есть a == b оценивается как False, потому что он действительно запускает a is b, тест идентичности (т.е. "Is a тот же объект, что и b?" ).

Когда определены __eq__ и __ne__, вы найдете это поведение (это то, за которым мы после):

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

Ответ 1

Рассмотрим эту простую задачу:

class Number:

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


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

n1 == n2 # False -- oops

Таким образом, Python по умолчанию использует идентификаторы объектов для операций сравнения:

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

Переопределение функции __eq__ похоже, решает проблему:

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

В Python 2 всегда помните о переопределении функции __ne__, так как в документации указано:

Между операторами сравнения нет подразумеваемых отношений. Истина x==y не означает, что x!=y ложно. Соответственно, при определении __eq__() следует также определить __ne__() чтобы операторы __ne__() образом.

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


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

В Python 3 это больше не требуется, поскольку в документации указано:

По умолчанию __ne__() делегирует __eq__() и инвертирует результат, если не является NotImplemented. Других подразумеваемых отношений между операторами сравнения нет, например, истина (x<y or x==y) не означает x<=y.

Но это не решает всех наших проблем. Давайте добавим подкласс:

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

Примечание. Python 2 имеет два типа классов:

  • классические (или старые) классы, которые не наследуются от object и объявляются как class A: class A(): или class A(B): где B - класс классического стиля;

  • классы нового стиля, которые наследуют от object и объявляются как class A(object) или class A(B): где B - класс нового стиля. Python 3 имеет только классы нового стиля, объявленные как class A: class A(object): или class A(B):

Для классов классического класса операция сравнения всегда вызывает метод первого операнда, тогда как для классов нового стиля он всегда вызывает метод операнда подкласса, независимо от порядка операндов.

Итак, если Number является классом классического стиля:

  • n1 == n3 вызывает n1.__eq__;
  • n3 == n1 вызывает n3.__eq__;
  • n1 != n3 вызывает n1.__ne__;
  • n3 != n1 вызывает n3.__ne__.

И если Number является классом нового стиля:

  • оба n1 == n3 и n3 == n1 вызывают n3.__eq__;
  • оба n1 != n3 и n3 != n1 вызывают n3.__ne__.

Чтобы исправить проблему несовместимости операторов == и != Для классов классического стиля Python 2, методы __eq__ и __ne__ должны возвращать значение NotImplemented если тип операнда не поддерживается. Документация определяет значение NotImplemented как:

Числовые методы и богатые методы сравнения могут вернуть это значение, если они не реализуют операцию для предоставленных операндов. (Интерпретатор затем попробует отраженную операцию или какой-либо другой резерв, в зависимости от оператора.) Его истинное значение истинно.

В этом случае оператор делегирует операцию сравнения методу отражения другого операнда. Документация определяет отраженные методы как:

Вариантов этих методов нет (для использования, когда левый аргумент не поддерживает операцию, но правильный аргумент); скорее, __lt__() и __gt__() являются отражением друг друга, __le__() и __ge__() являются отражением друг друга, а __eq__() и __ne__() являются их собственным отражением.

Результат выглядит следующим образом:

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

Возвращение значения NotImplemented вместо False - это правильная вещь, которую нужно делать даже для классов нового стиля, если требуется, чтобы операнды операторов == и != Были необходимы, когда операнды имеют несвязанные типы (без наследования).

Мы уже на месте? Не совсем. Сколько у нас уникальных номеров?

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

Наборы используют хеши объектов, и по умолчанию Python возвращает хэш идентификатора объекта. Давайте попробуем переопределить его:

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

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

Конечный результат выглядит следующим образом (я добавил некоторые утверждения в конце для проверки):

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

Ответ 2

Вам нужно быть осторожным с наследованием:

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

Более строго проверьте типы, например:

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

Кроме того, ваш подход будет работать отлично, что специальные методы для него.

Ответ 3

То, как вы описываете, - это то, как я всегда это делал. Поскольку он полностью общий, вы всегда можете сломать эту функциональность в классе mixin и наследовать ее в классах, где вы хотите эту функциональность.

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

Ответ 4

Не прямой ответ, но, казалось, достаточно уместным, чтобы его можно было использовать, так как иногда это спасает от многословной скуки. Вырезать прямо из документов...


functools.total_ordering (ЦБС)

Учитывая класс, определяющий один или несколько методов упорядочения с обогащенным сравнением, этот декоратор класса предоставляет остальное. Это упрощает усилия, связанные с указанием всех возможных операций с обогащенным сравнением:

Класс должен определять один из __lt__(), __le__(), __gt__() или __ge__(). Кроме того, класс должен предоставить метод __eq__().

Новое в версии 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()))

Ответ 5

Вам не нужно переопределять как __eq__, так и __ne__, вы можете переопределить только __cmp__, но это сделает импликацию на результат ==,! ==, <, > и т.д.

is тесты для идентификации объекта. Это означает, что is b будет True в случае, когда a и b имеют ссылку на один и тот же объект. В python вы всегда держите ссылку на объект в переменной, а не на фактическом объекте, поэтому по существу для a это b, чтобы быть истинным, объекты в них должны располагаться в одном и том же месте памяти. Как и что самое главное, почему вы решили переопределить это поведение?

Изменить: я не знал, что __cmp__ был удален из python 3, поэтому избегайте его.

Ответ 6

Из этого ответа: fooobar.com/questions/5003/... Я продемонстрировал, что, хотя правильно определить __ne__ в терминах __eq__ - вместо

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

вы должны использовать:

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

Ответ 7

Я думаю, что два слова, которые вы ищете, это равенство (==) и identity (is). Например:

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

Ответ 8

Тест 'is' будет проверять идентификацию с помощью встроенной функции id(), которая по существу возвращает адрес памяти объекта и, следовательно, не является перегружаемой.

Однако в случае тестирования равенства класса вы, вероятно, хотите быть немного более строгим в своих тестах и ​​только сравнить атрибуты данных в своем классе:

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

Этот код будет сравнивать только элементы данных не функциональных данных вашего класса, а также пропускать что-либо личное, что обычно вам нужно. В случае Plain Old Python Objects у меня есть базовый класс, который реализует __init__, __str__, __repr__ и __eq__, поэтому мои объекты POPO не несут бремя всей этой дополнительной (и в большинстве случаев идентичной) логики.

Ответ 9

Вместо того, чтобы использовать подклассы/миксины, мне нравится использовать универсальный декоратор классов

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

Использование:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x

a = Number(1)
b = Number(1)
assert a == b