Почему/когда в Python `x==y` вызывает `y.__eq__(x)`?

В документации Python четко указано, что x==y вызывает x.__eq__(y). Однако кажется, что во многих случаях верно обратное. Где задокументировано, когда и почему это происходит, и как я могу точно определить, будут ли вызываться методы __cmp__ или __eq__ моего объекта.

Изменить: Просто чтобы уточнить, я знаю, что __eq__ вызывается в предпочтении к __cmp__, но мне не ясно, почему y.__eq__(x) вызывается в предпочтении к x.__eq__(y), когда последнее - это то, что произойдет в состоянии документов.

>>> class TestCmp(object):
...     def __cmp__(self, other):
...         print "__cmp__ got called"
...         return 0
... 
>>> class TestEq(object):
...     def __eq__(self, other):
...         print "__eq__ got called"
...         return True
... 
>>> tc = TestCmp()
>>> te = TestEq()
>>> 
>>> 1 == tc
__cmp__ got called
True
>>> tc == 1
__cmp__ got called
True
>>> 
>>> 1 == te
__eq__ got called
True
>>> te == 1
__eq__ got called
True
>>> 
>>> class TestStrCmp(str):
...     def __new__(cls, value):
...         return str.__new__(cls, value)
...     
...     def __cmp__(self, other):
...         print "__cmp__ got called"
...         return 0
... 
>>> class TestStrEq(str):
...     def __new__(cls, value):
...         return str.__new__(cls, value)
...     
...     def __eq__(self, other):
...         print "__eq__ got called"
...         return True
... 
>>> tsc = TestStrCmp("a")
>>> tse = TestStrEq("a")
>>> 
>>> "b" == tsc
False
>>> tsc == "b"
False
>>> 
>>> "b" == tse
__eq__ got called
True
>>> tse == "b"
__eq__ got called
True

Изменить: из ответа и комментария Марка Дикинсона может показаться, что:

  1. Расширенное сравнение переопределяет __cmp__
  2. __eq__ является собственным __rop__ для своего __op__ (и аналогично для __lt__, __ge__ и т. д.)
  3. Если левый объект является встроенным классом или классом нового стиля, а правый является его подклассом, то __rop__ правого объекта пробуется перед __op__ левого объекта.

Это объясняет поведение в примерахTestStrCmp. TestStrCmp является подклассом str, но не реализует свой собственный __eq__, поэтому __eq__ из str имеет приоритет в обоих случаях (т.е. tsc == "b" вызывает b.__eq__(tsc) как __rop__ из-за правила 1).

В примерах TestStrEq tse.__eq__ вызывается в обоих случаях, потому что TestStrEq является подклассом str и поэтому вызывается предпочтительно.

В примерах TestEq TestEq реализует __eq__, а int нет, поэтому __eq__ вызывается оба раза (правило 1).

Но я до сих пор не понимаю самого первого примера с TestCmp. tc не является подклассом int, поэтому следует вызывать AFAICT 1.__cmp__(tc), но это не так.


person Singletoned    schedule 17.02.2010    source источник


Ответы (4)


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

См. документацию по адресу:

http://docs.python.org/reference/datamodel.html#coercion-rules

и, в частности, следующие два абзаца:

Для объектов x и y сначала пробуется x.__op__(y). Если это не реализовано или возвращает NotImplemented, пробуется y.__rop__(x). Если это также не реализовано или возвращает NotImplemented, возникает исключение TypeError. Но см. следующее исключение:

Исключение из предыдущего пункта: если левый операнд является экземпляром встроенного типа или класса нового стиля, а правый операнд является экземпляром надлежащего подкласса этого типа или класса и переопределяет базовый метод __rop__(), метод правого операнда __rop__() пробуется перед методом __op__() левого операнда.

person Mark Dickinson    schedule 17.02.2010
comment
@Daniel Pryden: Спасибо за исправления форматирования! В следующий раз постараюсь вспомнить цитату. - person Mark Dickinson; 17.02.2010
comment
Хороший, однако я думал (но не уверен), что все методы __rop__ устарели. Также я не использую ни один из них. - person Singletoned; 17.02.2010
comment
Согласен, что вы не используете никаких __rop__ методов. Методы сравнения в этом отношении особенные: __eq__ является обратным, поэтому читайте __eq__ как для __op__, так и для __rop__. (Точно так же, __ne__ является его собственным обратным, __le__ является обратным __ge__ и т. д.) Другие уже комментировали (правильно, IMO), что документацию здесь можно было бы немного доработать. Я почти уверен, что методы __rop__ не устарели! - person Mark Dickinson; 17.02.2010
comment
Если вы правы, то это ответ, который я бы принял. У вас есть доказательства того, что это правда? - person Singletoned; 18.02.2010
comment
Вы правы, и это определенно не помешало бы прояснить ситуацию. Я перечитал эту страницу несколько раз, и до меня не дошло, что там говорится, что __lt__ является __rop__ из __gt__. Тоже не так ли? Разве __lt__ не должно быть __rop__ из __ge__? ИП (2<2)==(not(2>=2)) - person Singletoned; 18.02.2010
comment
Также мы еще не совсем объяснили самый первый пример: 1 == tc. Единственное объяснение, которое я вижу для ATM, заключается в том, что подкласс object считается подклассом любого встроенного в целях перегрузки оператора. - person Singletoned; 18.02.2010
comment
Нет, я думаю, это правильно: __lt__ это __rop__ из __gt__. Логического отрицания не происходит; просто перестановка аргументов. Перевод: x.__lt__(y) ‹=› x < y ‹=› y > x ‹=› y.__gt__(x). - person Mark Dickinson; 18.02.2010
comment
Правила для __cmp__ (особенно в сочетании с богатыми сравнениями) действительно запутаны, и я не думаю, что они даже должным образом задокументированы где-либо, кроме исходного кода. (PyObject_RichCompare в файле Objects/object.c — это основное место, где можно посмотреть, если вы склонны.) В примере 1 == tc ни одна из сторон не реализует __eq__, поэтому мы возвращаемся к __cmp__. int.__cmp__ может обрабатывать только сравнения с другими целыми числами, поэтому он игнорируется (даже не вызывается), и вместо него вызывается ваш метод TestCmp.__cmp__. Избавление от __cmp__ в py3k было очень хорошей вещью. :) - person Mark Dickinson; 18.02.2010
comment
Что ж, я наградил вас ответом за чистую глубину ответа. Я предполагаю, что реальный ответ на мой вопрос никто не знает. Будем надеяться, что в Py3k все получится намного яснее. - person Singletoned; 19.02.2010

На самом деле, в документах говорится:

[__cmp__ вызывается] операциями сравнения, если расширенное сравнение (см. выше) не определено.

__eq__ — это расширенный метод сравнения, а в случае TestCmp он не определен, поэтому вызывается __cmp__.

person Dancrumb    schedule 17.02.2010
comment
Но str.__eq__ определено, поэтому предположительно определено (унаследовано) TestStrCmp.__eq__. - person dubiousjim; 17.02.2010
comment
Вы правы в том, что __eq__ переопределяет __cmp__, но это не было удивительным поведением. Сюрпризом было то, что он вызывает его на правом объекте, а не на левом. (Я обновил вопрос, чтобы немного прояснить это). - person Singletoned; 17.02.2010

Насколько я знаю, __eq__() — это так называемый метод «богатого сравнения», и он вызывается для операторов сравнения, а не __cmp__() ниже. __cmp__() вызывается, если "богатое сравнение" не определено.

Таким образом, в A == B:
Если __eq__() определено в A, оно будет вызываться
В противном случае будет вызываться __cmp__()

__eq__() определено в 'str', поэтому ваша функция __cmp__() не вызывалась.

То же правило для методов "богатого сравнения" __ne__(), __gt__(), __ge__(), __lt__() и __le__().

person Mikhail Churbanov    schedule 17.02.2010

Разве это не задокументировано в Справочнике по языку? Просто из беглого взгляда видно, что __cmp__ игнорируется, когда определены __eq__, __lt__ и т. д. Я понимаю, что нужно включить случай, когда __eq__ определен в родительском классе. str.__eq__ уже определено, поэтому __cmp__ в его подклассах будет игнорироваться. object.__eq__ и т. д. не определены, поэтому __cmp__ в его подклассах будет учитываться.

В ответ на уточненный вопрос:

Я знаю, что __eq__ вызывается в предпочтении к __cmp__, но мне не ясно, почему y.__eq__(x) вызывается в предпочтении к x.__eq__(y), когда последнее - это то, что произойдет в состоянии документов.

Документы говорят, что x.__eq__(y) будет вызываться первым, но у него есть возможность вернуть NotImplemented, и в этом случае вызывается y.__eq__(x). Не понимаю, почему вы уверены, что здесь происходит что-то другое.

Какой случай вас конкретно озадачил? Я так понимаю, вы просто озадачены делами "b" == tsc и tsc == "b", правильно? В любом случае вызывается str.__eq__(onething, otherthing). Поскольку вы не переопределяете метод __eq__ в TestStrCmp, в конечном итоге вы просто полагаетесь на метод базовой строки, и он говорит, что объекты не равны.

Не зная деталей реализации str.__eq__, я не знаю, вернет ли ("b").__eq__(tsc) NotImplemented и даст ли tsc шанс обработать тест на равенство. Но даже если бы это было так, как вы определили TestStrCmp, вы все равно получите ложный результат.

Так что не ясно, что вы видите здесь неожиданного.

Возможно, происходит то, что Python предпочитает __eq__ __cmp__, если он определен для любого из сравниваемых объектов, в то время как вы ожидали, что __cmp__ для крайнего левого объекта будет иметь приоритет над __eq__ для правого объекта. Это оно?

person dubiousjim    schedule 17.02.2010
comment
Поиграв с этим еще немного, я думаю, вы правы, что __eq__ предпочтительнее для любого объекта в этих случаях. - person Singletoned; 17.02.2010