Элегантные способы поддержки эквивалентности («равенства») в классах 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__?

Примечание: Небольшое пояснение - когда __eq__ и __ne__ не определены, вы обнаружите такое поведение:

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

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

Когда определены __eq__ и __ne__, вы обнаружите такое поведение (которое мы и ищем):

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

+1, поскольку я не знал, что dict использует пословное равенство для ==, я предположил, что он считает их равными только для тех же объектных dicts. Я думаю, это очевидно, поскольку в Python есть оператор is, позволяющий отличать идентичность объекта от сравнения значений.

SingleNegationElimination 12.07.2009 05:00

Я думаю, что принятый ответ будет исправлен или переназначен на ответ Алгориаса, чтобы была реализована строгая проверка типа.

max 04.10.2010 13:31

Также убедитесь, что хэш переопределен stackoverflow.com/questions/1608842/…

Alex Punnen 02.12.2015 11:51
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
454
3
232 920
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

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

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

Потому что иногда у вас есть другое определение равенства для ваших объектов.

Ed S. 24.12.2008 01:46

оператор is дает вам ответ интерпретатора на идентичность объекта, но вы по-прежнему можете выразить свое мнение о равенстве, переопределив cmp

Vasil 24.12.2008 01:49

В Python 3 «функция cmp () исчезла, а специальный метод __cmp __ () больше не поддерживается». is.gd/aeGv

gotgenes 24.12.2008 01:50

Я думаю, что вам нужны два термина: равенство (==) и личность (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

Возможно, за исключением того, что можно создать класс, который сравнивает только первые два элемента в двух списках, и если эти элементы равны, он оценивает значение True. Я считаю, что это эквивалентность, а не равенство. Совершенно верно в экв, все еще.

gotgenes 24.12.2008 02:23

Однако я согласен с тем, что «есть» - это проверка идентичности.

gotgenes 24.12.2008 02:24

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

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

+1: шаблон стратегии, позволяющий легко заменять подклассы.

S.Lott 24.12.2008 17:14

isinstance отстой. Зачем это проверять? Почему не просто self .__ dict__ == other .__ dict__?

nosklo 26.12.2008 21:56

@nosklo: Я не понимаю .. что, если два объекта из совершенно несвязанных классов имеют одинаковые атрибуты?

max 04.10.2010 13:27

@max nosklo хорошо замечает. При подклассе встроенных объектов учитывайте поведение по умолчанию. Оператору == безразлично, сравниваете ли вы встроенное с подклассом встроенного.

gotgenes 05.10.2010 19:35

Думал, ноксло предложил пропустить isinstance. В этом случае вы больше не знаете, принадлежит ли other подклассу self.__class__.

max 05.10.2010 19:57

@max: да, но не имеет значения, подкласс это или нет. Это не относящаяся к делу информация. Подумайте, что произойдет, если это не подкласс.

nosklo 07.10.2010 08:04

@nosklo: если он не подкласс, но случайно имеет те же атрибуты, что и self (как ключи, так и значения), __eq__ может оцениваться как True, хотя это бессмысленно. Я что-нибудь упускаю?

max 07.10.2010 12:10

@nosklo: Да, может быть, hasattr(other, '__dict__') and self.__dict__ == other.__dict__ в общем случае подойдет лучше. Думаю, я просто предпочитаю более строгое понятие равенства, учитывая возможность.

cdleary 08.10.2010 01:56

Другая проблема со сравнением __dict__ заключается в том, что если у вас есть атрибут, который вы не хотите учитывать в своем определении равенства (скажем, например, уникальный идентификатор объекта или метаданные, такие как отметка времени создания).

Adam Parkin 02.05.2012 01:48

@cdleary: Я вижу, что hasattr предотвратит исключение, когда other не имеет __dict__ (т.е. реализован со слотами). Но как это связано с вопросом @nosklo?

max 21.09.2012 09:21

Я думаю, реализация этого во всех ваших классах может привести к бесконечной рекурсии с круговыми ссылками (например, установить a.b = b, b.a = a, a == a)

Lars 20.09.2013 11:37

Обратите внимание, что у этого есть некоторые проблемы с наследованием, обязательно проверьте решение это!

Robin 24.03.2014 10:48

Кроме того, отсутствие проверки в hasattr(self, '__dict__') вызовет исключение при сравнении с None. Довольно явная дыра в реализации.

Sandy Chapman 06.05.2015 14:10

TypeError: нехешируемый тип: 'dict'

soulmachine 05.10.2015 12:21

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

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

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

Это хороший момент. Я полагаю, стоит отметить, что подклассы встроенных типов по-прежнему допускают равенство в любом направлении, и поэтому проверка того, что это один и тот же тип, может быть даже нежелательна.

gotgenes 06.08.2009 01:26

Я бы предложил вернуть NotImplemented, если типы разные, делегируя сравнение правым.

max 21.09.2012 10:40

@max сравнение не обязательно выполняется слева (LHS) с правой (RHS), затем с RHS на LHS; см. stackoverflow.com/a/12984987/38140. Тем не менее, возврат NotImplemented, как вы предлагаете, всегда будет вызывать superclass.__eq__(subclass), что является желаемым поведением.

gotgenes 15.05.2013 00:10

это тоже быстрее, потому что isinstance может быть немного медленным

Lars 20.09.2013 11:29

Если у вас много участников и не так много копий объектов, то обычно хорошо добавить начальный тест идентичности if other is self. Это позволяет избежать более длинного сравнения словарей и может дать огромную экономию, когда объекты используются в качестве ключей словаря.

Dane White 03.12.2013 04:18

И не забудьте реализовать __hash__()

Dane White 03.12.2013 04:25

Я думаю, что это должен быть принятый ответ, потому что он фактически отвечает на конкретный вопрос о __dict__.

mrexodia 23.02.2020 13:59

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

Wouter Lievens 04.01.2021 11:58

Тест 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

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

Немного придирчиво, но тесты «есть» с использованием id () только в том случае, если вы не определили свою собственную функцию-член is_ () (2.3+). [docs.python.org/library/operator.html]

spenthil 04.10.2010 02:02

Я предполагаю, что под «переопределением» вы на самом деле имеете в виду исправление модуля оператора. В этом случае ваше утверждение не совсем точное. Модуль операторов предоставляется для удобства, и переопределение этих методов не влияет на поведение оператора is. Сравнение с использованием «is» всегда использует id () объекта для сравнения, это поведение не может быть изменено. Также функция-член is_ не влияет на сравнение.

mcrute 04.10.2010 04:11

mcrute - я заговорил слишком рано (и неправильно), вы абсолютно правы.

spenthil 04.10.2010 06:22

Это очень хорошее решение, особенно когда __eq__ будет объявлен в CommonEqualityMixin (см. Другой ответ). Я нашел это особенно полезным при сравнении экземпляров классов, производных от Base в SQLAlchemy. Чтобы не сравнивать _sa_instance_state, я заменил key.startswith("__")): на key.startswith("_")):. У меня также были некоторые обратные ссылки, и ответ от Algorias генерировал бесконечную рекурсию. Поэтому я назвал все обратные ссылки, начиная с '_', чтобы они также пропускались при сравнении. ПРИМЕЧАНИЕ: в Python 3.x измените iteritems() на items().

Wookie88 29.05.2013 16:11

@mcrute Обычно __dict__ экземпляра не имеет ничего, что начинается с __, если это не было определено пользователем. Такие вещи, как __class__, __init__ и т. д., Находятся не в __dict__ экземпляра, а в его классе __dict__. OTOH, частные атрибуты могут легко начинаться с __ и, вероятно, должны использоваться для __eq__. Не могли бы вы уточнить, чего именно вы пытались избежать при пропуске атрибутов с префиксом __?

max 13.04.2015 15:44

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


functools.total_ordering (cls)

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

Класс должен определять один из __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()))

Однако у total_ordering есть небольшие подводные камни: regebro.wordpress.com/2010/12/13/…. Будьте внимательны!

Mr_and_Mrs_D 31.05.2016 15:28
Ответ принят как подходящий

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

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__, поскольку документация утверждает:

There are no implied relationships among the comparison operators. The truth of x==y does not imply that x!=y is false. Accordingly, when defining __eq__(), one should also define __ne__() so that the operators will behave as expected.

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 в этом больше нет необходимости, поскольку документация утверждает:

By default, __ne__() delegates to __eq__() and inverts the result unless it is NotImplemented. There are no other implied relationships among the comparison operators, for example, the truth of (x<y or x==y) does not imply 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 как:

Numeric methods and rich comparison methods may return this value if they do not implement the operation for the operands provided. (The interpreter will then try the reflected operation, or some other fallback, depending on the operator.) Its truth value is true.

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

There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather, __lt__() and __gt__() are each other’s reflection, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection.

Результат выглядит так:

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

Возвращение значения 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
hash(tuple(sorted(self.__dict__.items()))) не будет работать, если среди значений self.__dict__ есть какие-либо нехешируемые объекты (то есть, если какой-либо из атрибутов объекта установлен, например, на list).
max 15.04.2015 14:25

Верно, но тогда, если у вас есть такие изменяемые объекты в вашем vars (), два объекта на самом деле не равны ...

Tal Weiss 15.04.2015 14:34

Отличное резюме, но ты должен реализовать __ne__ с использованием == вместо __eq__.

Florian Brucker 21.06.2015 23:24

Три замечания: 1. В Python 3 больше нет необходимости реализовывать __ne__: «По умолчанию __ne__() делегирует __eq__() и инвертирует результат, если это не NotImplemented». 2. Если кто-то все еще хочет реализовать __ne__, более общая реализация (которая, как мне кажется, используется в Python 3): x = self.__eq__(other); if x is NotImplemented: return x; else: return not x. 3. Приведенные реализации __eq__ и __ne__ являются субоптимальными: if isinstance(other, type(self)): дает 22 вызова __eq__ и 10 вызовов __ne__, тогда как if isinstance(self, type(other)): дает 16 вызовов __eq__ и 6 вызовов __ne__.

Maggyero 09.11.2017 15:58

Хотя этот ответ в конечном итоге достигает правильной реализации, он попадает туда очень запутанным образом. Он неоднократно показывает неправильный код таким образом, что предлагает читателям подумать, что их проблемы решены, и перестать читать, и он говорит, что другие ответы не работают, а затем запускает описание проблем, которые обрабатывают другие ответы делать, прежде чем переходить к частям, которые они не используют т. Кроме того, здесь не упоминаются случаи, когда вы должны установить __hash__ на None вместо того, чтобы реализовывать его.

user2357112 supports Monica 14.11.2017 22:21

Проверка isinstance (other, ...) была очень полезна при работе с проверками на None. Спасибо!

brownmagik352 23.01.2019 09:07

Он спросил об элегантности, но он стал крепким.

GregNash 03.04.2019 22:36
n1 == n3 тоже должен быть True даже для классического класса? Потому что в этом случае other должен быть n3, а isinstance(n3, Number) - True?
Bin 26.05.2019 12:07

Это не отвечает на вопрос.

mrexodia 23.02.2020 14:05

Из этого ответа: https://stackoverflow.com/a/30676267/541136 Я продемонстрировал это, хотя правильно определять __ne__ в терминах __eq__ - вместо

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

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

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

Вместо использования подклассов / миксинов я предпочитаю использовать декоратор общего класса.

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

Это включает комментарии к ответу Алгориаса и сравнивает объекты по одному атрибуту, потому что меня не волнует весь dict. hasattr(other, "id") должен быть истинным, но я знаю, что это потому, что я установил его в конструкторе.

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

Другие вопросы по теме