__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
Дивіться мою відповідь на ваше останнє запитання, але насправді є дизайн, який полегшить роботу навіть для багатьох класів, включаючи ваш (зараз для його застосування потрібен мікс, метаклас або декоратор класу): якщо ключовий спеціальний метод присутній, він повинен повернути кортеж значень, і всі компаратори AND хеш визначаються в рамках цього кортежу. Гвідо сподобався моїй ідеї, коли я йому пояснив, але потім я зайнявся іншими речами і ніколи не замислювався писати ПЕП ... можливо, за 3,2 ;-). Тим часом я продовжую використовувати міксин для цього! -)
Алекс Мартеллі

Відповіді:


90

Так, легко реалізувати все з точки зору, наприклад, __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__він повинен визначити їх безпосередньо , так Виконання підмішати в не використовують (наприклад, це має місце для dict) - насправді __ne__цілком може бути визначено для полегшення що як:

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

але в наведеному вище коді я хотів зберегти приємну симетрію лише використання <;-). Щодо того , чому __cmp__повинен був піти, так як ми зробили є __lt__і друзі, навіщо тримати інший, інший спосіб зробити те ж саме навколо? Це всього стільки мертвої ваги за кожен час виконання Python (Classic, Jython, IronPython, PyPy, ...). Код, у якому точно не буде помилок, - це код, якого немає - звідки в принципі Python, що в ідеалі повинен бути один очевидний спосіб виконання завдання (C має той же принцип у розділі "Spirit of C" стандарт ISO, btw).

Це не означає, що ми не можемо заборонити речі (наприклад, майже еквівалентність між комбінаціями та декораторами класів для деяких застосувань), але це, безумовно , означає, що нам не подобається переносити код у компіляторах та / або час виконання, який існує надто просто для підтримки декількох еквівалентних підходів до виконання однакових завдань.

Подальше редагування: насправді є ще кращий спосіб порівняння І хешування для багатьох класів, включаючи те, що у запитанні - __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, а також як здійснити це за допомогою міксину).
Алекс Мартеллі

Мені дуже подобається ця ключова ідея, збираюся її використати і подивитися, як вона почувається. (Хоч імені 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()))

9
total_orderingне реалізує, __ne__хоча стежте!
Flimm

3
@Flimm, це не так, але __ne__. але це тому, що __ne__має за замовчуванням реалізацію, яку делегує __eq__. Тож тут нема чого слідкувати.
Ян Худек

повинні визначити принаймні одну операцію замовлення: <> <=> = .... eq не потрібен як загальний порядок, якщо! a <b і b <a тоді a = b
Ксаллантас

9

Це стосується PEP 207 - Різні порівняння

Крім того, __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
Re 1, якщо вони не мають сенсу, не застосовуйте cmp . Re 2, маючи обидва варіанти, може дозволити вам оптимізувати за потребою, при цьому все ще швидко прототипуючи та тестуючи. Ніхто не каже мені, чому його видаляють. (По суті, це зводиться до ефективності для мене розробника.) Чи можливо, що багаті порівняння є менш ефективними, якщо відмітка cmp є на місці? Це не мало б сенсу для мене.

1
@R. Пат, як я намагаюся пояснити у своїй відповіді, немає справжньої втрати в цілому (оскільки міксин, декоратор або метаклас дозволяє легко визначати все з точки зору просто <якщо ви хочете), і так для всіх реалізації Python для виконання надлишковий код, що повертається до cmp назавжди - просто щоб користувачі Python висловлювали речі двома еквівалентними способами, - на 100% зіткнулися б із зерном Python.
Алекс Мартеллі

2

(Відредаговано 6/17/17 для врахування коментарів.)

Я випробував порівнянну відповідь Mixin вище. Я зіткнувся з неприємностями з "Ніхто". Ось модифікована версія, яка обробляє порівняння рівності з "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.
Antti Haapala

3
selfНЕ буде ніколи бути None, так що галузь може піти повністю. Не використовуйте type(other) == type(None); просто використовувати other is None. Замість спеціального корпусу None, випробування , якщо інший тип є екземпляром типу self, і повертає NotImplementedСінглтон , якщо немає: if not isinstance(other, type(self)): return NotImplemented. Зробіть це для всіх методів. Тоді Python зможе надати іншому операнду можливість надати відповідь.
Martijn Pieters

1

Натхненний Алекс Мартелл - х ComparableMixinі KeyedMixinвідповіді, я придумав наступний Mixin. Це дозволяє реалізувати єдиний _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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.