При сравнении объектов метод eq возвращает NotImplemented: как Python выбирает дальнейшее действие?

При сравнении объектов метод eq возвращает NotImplemented: как Python выбирает дальнейшее действие?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

NotImplemented означает не «сравнение ложно», а «этот метод не умеет сравнивать данные операнды». Для a == b Python пытается использовать подходящую реализацию сравнения с учётом типов операндов; если обе стороны вернули NotImplemented, результатом обычно становится сравнение по идентичности.

Исторический контекст

Модель данных Python позволяет объектам определять поведение операторов через специальные методы, не заставляя сам оператор знать конкретные классы. Для бинарных операций и сравнений этого недостаточно: два разных типа могут поддерживать совместное сравнение, поэтому протоколу нужен способ сообщить «операция для этой пары типов не поддерживается».

Именно эту роль выполняет NotImplemented. Он позволяет передать попытку другой реализации или встроенному механизму Python, не маскируя отсутствие поддержки обычным значением False.

Постановка проблемы

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

Если вместо NotImplemented выбросить TypeError, сравнение немедленно завершится исключением. Такой вариант оправдан только тогда, когда неподдерживаемый тип означает ошибку контракта, а не обычную несовместимость операндов.

Подробное решение

При выражении a == b Python сначала выбирает реализацию __eq__. Если она возвращает результат, он используется как результат сравнения; это может быть не только bool, поскольку Python допускает произвольный объект результата, преобразуемый к логическому значению при необходимости.

Если реализация возвращает NotImplemented, Python пробует другую сторону. Для равенства это также __eq__ правого операнда. Если тип правого операнда является строгим подклассом типа левого операнда, его реализация может получить приоритет, поскольку подкласс потенциально задаёт более специальное правило сравнения.

Если обе реализации вернули NotImplemented, для == Python использует проверку идентичности: выражение истинно, когда операнды являются одним объектом. Для != применяется обратный результат. Это отличается от упорядочивающих сравнений: если обе стороны не поддержали, например, <, Python обычно возбуждает TypeError.

Минимальный пример:

class Token: def __init__(self, value): self.value = value def __eq__(self, other): if not isinstance(other, Token): return NotImplemented return self.value == other.value class External: def __eq__(self, other): return isinstance(other, Token) print(Token(1) == External()) # True

Сначала Token.__eq__ сообщает, что не поддерживает External. После этого Python вызывает External.__eq__, и тот возвращает True. Если бы Token.__eq__ вернул False, до правой стороны выполнение не дошло бы.

Правильное правило реализации обычно такое: для поддерживаемого типа вернуть результат сравнения, для неподдерживаемого — NotImplemented. Проверка через isinstance должна соответствовать реальному контракту класса; слишком широкая проверка может привести к сравнению объектов, для которых совпадение полей ничего не означает.

Ситуация из практики

В библиотеке есть класс значения Money и сторонний класс Decimal. Разработчик реализовал Money.__eq__ так, что любой объект, не являющийся Money, даёт False. В результате сторонний тип не может корректно реализовать симметричное сравнение с Money: левая сторона уже завершила операцию значением False.

Возможны два решения. Жёсткий TypeError явно запрещает смешанное сравнение, но ухудшает совместимость и требует от вызывающего кода предварительной проверки типов. Возврат NotImplemented сохраняет стандартный протокол Python и позволяет правому операнду принять решение.

Выбран второй вариант, потому что смешанное сравнение допустимо контрактом библиотеки. После изменения Money.__eq__ неизвестные типы корректно передаются другой стороне, а при отсутствии поддержки обеими сторонами сохраняется стандартное поведение Python.

Что кандидаты часто упускают

  1. Является ли NotImplemented исключением или логическим значением?

    Это специальное значение, а не исключение и не синоним False. Его возвращают из метода, чтобы интерпретатор продолжил протокол операции. Если вернуть его непосредственно пользователю из явно вызванного obj.__eq__(other), автоматического продолжения уже не будет: прямой вызов просто вернёт объект NotImplemented.

  2. Всегда ли после NotImplemented вызывается метод правого операнда?

    Нет, нужно учитывать правила выбора реализации и типы операндов. Для равенства Python может попробовать реализацию правого операнда, а при наличии более специального подкласса его метод получает приоритет. Если подходящая реализация уже вернула обычный результат, дальнейший вызов не выполняется.

  3. Почему нельзя бездумно возвращать NotImplemented для любого неподходящего объекта?

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