Подтвердить что ты не робот

__lt__ вместо __cmp__

Python 2.x имеет два способа перегрузить операторы сравнения: __cmp__ или "богатые операторы сравнения", такие как __lt__. Говорят, что предпочтительными являются богатые сравнительные перегрузки, но почему это так?

Богатые операторы сравнения проще реализовать каждый, но вы должны реализовать несколько из них с почти одинаковой логикой. Однако, если вы можете использовать встроенный порядок cmp и кортеж, то __cmp__ становится довольно простым и выполняет все сравнения:

class A(object):
  def __init__(self, name, age, other):
    self.name = name
    self.age = age
    self.other = other
  def __cmp__(self, other):
    assert isinstance(other, A) # assumption for this example
    return cmp((self.name, self.age, self.other),
               (other.name, other.age, other.other))

Эта простота, похоже, удовлетворяет мои потребности намного лучше, чем перегрузка всех 6 (!) богатых сравнений. (Тем не менее, вы можете получить его до "просто" 4, если вы полагаетесь на "измененный аргумент" /отраженное поведение, но это приводит к чистому увеличению сложности, по моему скромному мнению.)

Есть ли непредвиденные ошибки, о которых мне нужно знать, если я только перегружаю __cmp__?

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

Обновление: Как указано Christopher , cmp исчезает в 3.x. Существуют ли какие-либо альтернативы, которые делают сравнение реализации так же просто, как выше __cmp__?

4b9b3361

Ответ 1

Да, легко реализовать все с точки зрения, например, __lt__ с классом mixin (или метаклассом или декоратором класса, если ваш вкус работает именно так).

Например:

class ComparableMixin:
  def __eq__(self, other):
    return not self<other and not other<self
  def __ne__(self, other):
    return self<other or other<self
  def __gt__(self, other):
    return other<self
  def __ge__(self, other):
    return not self<other
  def __le__(self, other):
    return not other<self

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

Конечно, если ваш класс имеет особенно быстрый способ реализации (например) __eq__ и __ne__, он должен определять их напрямую, чтобы версии mixin не использовались (например, это относится к dict) - на самом деле __ne__ вполне можно определить, чтобы облегчить это как:

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

но в коде выше я хотел сохранить симпатичную симметрию только с помощью <;-). Что касается того, почему __cmp__ должен был уйти, так как у нас были __lt__ и друзья, зачем хранить другой, другой способ делать то же самое? Это так много мертвого веса в каждой среде исполнения Python (Classic, Jython, IronPython, PyPy,...). Код, в котором определенно не будет иметь ошибок, - это код, которого нет - откуда принцип Python должен быть идеальным одним очевидным способом выполнения задачи (C имеет тот же принцип в Раздел "Дух C" стандарта ISO, кстати).

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

Дальнейшее редактирование: на самом деле есть еще лучший способ обеспечить сравнение и хэширование для многих классов, в том числе в вопросе - метод __key__, как я упомянул в своем комментарии к вопросу. Поскольку я никогда не собирался писать PEP для этого, вы должны в настоящее время реализовать его с помощью Mixin (& c), если вам это нравится:

class KeyedMixin:
  def __lt__(self, other):
    return self.__key__() < other.__key__()
  # and so on for other comparators, as above, plus:
  def __hash__(self):
    return hash(self.__key__())

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

Ответ 2

Чтобы упростить этот случай, есть декоратор класса в Python 2.7 +/3.2 +, functools.total_ordering, который можно использовать для реализации того, что Алекс предлагает. Пример из документов:

@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()))

Ответ 4

(Отредактировано 6/17/17, чтобы принимать во внимание комментарии.)

Я опробовал сопоставимый ответ mixin выше. Я столкнулся с проблемой "Нет". Ниже приведена модифицированная версия, которая обрабатывает сравнение сравнений с "Нет". (Я не видел причин беспокоиться о сравнении неравенства с None как отсутствующей семантикой):


class ComparableMixin(object):

    def __eq__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other and not other<self

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

    def __gt__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return other<self

    def __ge__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other

    def __le__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not other<self    

Ответ 5

Вдохновленный ответами Alex Martelli ComparableMixin и KeyedMixin, я придумал следующий микс. Он позволяет реализовать один метод _compare_to(), который использует сопоставления на основе ключевых слов аналогично KeyedMixin, но позволяет вашему классу выбрать наиболее эффективный ключ сравнения на основе типа other. (Обратите внимание, что этот mixin не очень помогает для объектов, которые могут быть проверены на равенство, но не на порядок).

class ComparableMixin(object):
    """mixin which implements rich comparison operators in terms of a single _compare_to() helper"""

    def _compare_to(self, other):
        """return keys to compare self to other.

        if self and other are comparable, this function 
        should return ``(self key, other key)``.
        if they aren't, it should return ``None`` instead.
        """
        raise NotImplementedError("_compare_to() must be implemented by subclass")

    def __eq__(self, other):
        keys = self._compare_to(other)
        return keys[0] == keys[1] if keys else NotImplemented

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

    def __lt__(self, other):
        keys = self._compare_to(other)
        return keys[0] < keys[1] if keys else NotImplemented

    def __le__(self, other):
        keys = self._compare_to(other)
        return keys[0] <= keys[1] if keys else NotImplemented

    def __gt__(self, other):
        keys = self._compare_to(other)
        return keys[0] > keys[1] if keys else NotImplemented

    def __ge__(self, other):
        keys = self._compare_to(other)
        return keys[0] >= keys[1] if keys else NotImplemented