Элегантные способы поддержки эквивалентности («равенства») в классах Python - PullRequest
359 голосов
/ 24 декабря 2008

При написании пользовательских классов часто важно разрешить эквивалентность с помощью операторов == и !=. В 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__ не определены, вы обнаружите следующее:

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

Ответы [ 9 ]

261 голосов
/ 07 августа 2014

Рассмотрим эту простую проблему:

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__(), чтобы операторы будут вести себя как положено.

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 как:

Числовые методы и методы расширенного сравнения могут возвращать это значение, если они не реализуют операцию для предоставленных операндов. (The Затем интерпретатор попытается отразить операцию или другую отступление, в зависимости от оператора.) Его истинное значение истинно.

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

Нет версий этих методов со свопированными аргументами (которые будут использоваться когда левый аргумент не поддерживает операцию, но правый аргумент делает); скорее, __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

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

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
191 голосов
/ 24 декабря 2008

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

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

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

155 голосов
/ 24 декабря 2008

То, как вы описываете, - это то, как я всегда это делал. Поскольку он полностью универсален, вы всегда можете разбить эту функциональность на класс 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
13 голосов
/ 19 сентября 2012

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


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()))
8 голосов
/ 24 декабря 2008

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

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

Редактировать: я не знал, что __cmp__ был удален из Python 3, поэтому избегайте его.

3 голосов
/ 17 июня 2015

Из этого ответа: 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
2 голосов
/ 24 декабря 2008

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

>>> 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
1 голос
/ 24 декабря 2008

Тест '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 не несут бремени всей этой дополнительной (и в большинстве случаев идентичной) логики.

0 голосов
/ 15 марта 2019

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

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...