__lt__ вместо __cmp__

100

В 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__?

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

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

Сообщество
источник
5
См. Мой ответ на ваш последний вопрос, но на самом деле есть дизайн, который упростит работу для многих классов, включая ваш (прямо сейчас вам нужен миксин, метакласс или декоратор классов, чтобы применить его): если присутствует ключевой специальный метод, он должен возвращать кортеж значений, и все компараторы И хэш определены в терминах этого кортежа. Гвидо понравилась моя идея, когда я объяснил ее ему, но потом я занялся другими вещами и так и не успел написать PEP ... может быть, для 3.2 ;-). Между тем, я продолжаю использовать для этого свой миксин! -)
Alex Martelli

Ответы:

90

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

Например:

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

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

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

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

Дальнейшее редактирование: на самом деле есть еще лучший способ обеспечить сравнение и хеширование для многих классов, включая тот __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__специальный метод обращается напрямую к тому, что нужно.

Алекс Мартелли
источник
Извините за задержку @R. Пэйт, я решил, что, поскольку мне все равно приходилось редактировать, я должен дать наиболее подробный ответ, который я мог, а не торопиться (и я только что отредактировал снова, чтобы предложить свою старую ключевую идею, которую я так и не понял, PEPping, а также то, как реализовать это с помощью миксина).
Alex Martelli
Мне очень нравится эта ключевая идея, я собираюсь использовать ее и посмотреть, что она собой представляет. (Хотя названо cmp_key или _cmp_key вместо зарезервированного имени.)
TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixinкогда я пробую это на Python 3. Смотрите полный код на gist.github.com/2696496
Адам Паркин,
2
В Python 2.7 + / 3.2 + вы можете использовать functools.total_orderingвместо создания своего собственного ComparableMixim. Как было предложено в ответе jmagnusson
День
4
Использовать <для реализации __eq__в Python 3 - довольно плохая идея из-за TypeError: unorderable types.
Антти Хаапала,
49

Чтобы упростить этот случай, в 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()))
Jmagnusson
источник
9
total_orderingне реализует __ne__, так что будьте осторожны!
Flimm
3
@Flimm, нет, но __ne__. но это потому, что __ne__есть реализация по умолчанию, которая делегирует __eq__. Так что здесь не на что смотреть.
Ян Худек
должен определять по крайней мере одну операцию упорядочивания: <> <=> = .... eq не требуется как общий порядок, если! a <b и b <a, тогда a = b
Ксанлантос
9

Это описано в PEP 207 - Rich Comparisons

Кроме того, __cmp__уходит в python 3.0. (Обратите внимание, что его нет на http://docs.python.org/3.0/reference/datamodel.html, но он ЕСТЬ на http://docs.python.org/2.7/reference/datamodel.html )

Кристофер
источник
PEP заботится только о том, зачем нужны богатые сравнения, подобно тому, как пользователи NumPy хотят, чтобы A <B возвращал последовательность.
Я не понимал, что это точно уходит, это меня огорчает. (Но спасибо, что указали на это.)
PEP также обсуждает, «почему» они предпочтительны. По сути, это сводится к эффективности: 1. Нет необходимости реализовывать операции, которые не имеют смысла для вашего объекта (например, неупорядоченные коллекции). 2. Некоторые коллекции имеют очень эффективные операции с некоторыми видами сравнений. Богатые сравнения позволяют интерпретатору использовать это преимущество, если вы их определяете.
Кристофер
1
К вопросу 1. Если они не имеют смысла, не внедряйте cmp . В отношении 2: наличие обоих вариантов позволит вам оптимизировать по мере необходимости, при этом быстро создавая прототип и тестируя. Никто не говорит мне, почему он удален. (По сути, для меня это сводится к эффективности разработчика.) Возможно ли, что богатые сравнения менее эффективны при наличии резервной копии cmp ? Для меня это не имело бы смысла.
1
@Р. Пейт, как я пытаюсь объяснить в своем ответе, нет никакой реальной потери в общности (поскольку миксин, декоратор или метакласс позволяет вам легко определять все в терминах просто <, если хотите), и поэтому для всех реализаций Python можно носить с собой избыточный код, возвращающийся к cmp навсегда - просто чтобы позволить пользователям Python выражать вещи двумя эквивалентными способами - будет работать на 100% против зерна Python.
Alex Martelli
2

(Отредактировано 17.06.17, чтобы учесть комментарии.)

Я попробовал сопоставимый ответ на миксин выше. У меня были проблемы с "None". Вот модифицированная версия, которая обрабатывает сравнения на равенство с «Нет». (Я не видел причин беспокоиться о сравнении неравенства с 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    
Габриэль Феррер
источник
Как вы думаете , что selfможет быть синглтон Noneиз NoneTypeи в то же время реализовать ваши ComparableMixin? И действительно, этот рецепт плох для Python 3.
Антти Хаапала
3
selfне будет никогда быть None, так что отрасль может пойти полностью. Не используйте type(other) == type(None); просто используйте other is None. Вместо специального корпуса None, испытание , если другой тип является экземпляром типа self, и возвращает NotImplementedсинглтон , если нет: if not isinstance(other, type(self)): return NotImplemented. Сделайте это для всех методов. Затем Python может дать другому операнду возможность дать ответ.
Мартейн Питерс
1

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

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
Эли Коллинз
источник