Сопоставимые классы в Python 3

Каков стандартный способ сделать класс сопоставимым в Python 3? (Например, по идентификатору.)


person Neil G    schedule 02.08.2011    source источник
comment
Я имею в виду использование id(obj) в качестве ключа для сортировки.   -  person Neil G    schedule 02.08.2011
comment
А зачем вам сортировать по произвольно назначенному адресу памяти вашего класса?   -  person Tim Pietzcker    schedule 02.08.2011
comment
конечно, вы могли, но имеет ли смысл упорядочивать объекты по их расположению в памяти?   -  person jcomeau_ictx    schedule 02.08.2011
comment
@Tim: Потому что я делаю: max((f(obj), obj) for obj in obj_list)[1] чтобы получить объект с наивысшим значением в соответствии с f. Python 3 жалуется, что obj несопоставим. Мне все равно, как это сравнивать.   -  person Neil G    schedule 02.08.2011
comment
Я не понимаю. Как вы можете хотеть найти наибольшее значение и в то же время не заботиться о том, как выполняется сравнение?   -  person Tim Pietzcker    schedule 02.08.2011
comment
Наверняка вы хотите сказать max(obj_list, key=f) вместо этого чудовища   -  person John La Rooy    schedule 02.08.2011
comment
@Tim Я хочу, чтобы obj было максимально f(obj). См. stackoverflow.com/questions/5098580/   -  person Neil G    schedule 02.08.2011
comment
@gnibbler: Спасибо!! Но что, если мне нужны как obj, так и f(obj)?   -  person Neil G    schedule 02.08.2011
comment
Просто вызовите f снова по результату. Это, вероятно, более эффективно, чем создание всех этих кортежей   -  person John La Rooy    schedule 02.08.2011


Ответы (5)


Чтобы сделать классы сопоставимыми, вам нужно только реализовать __lt__ и украсить класс functools.total_ordering. Вы также должны предоставить метод __eq__, если это возможно. Это обеспечивает остальные операторы сравнения, поэтому вам не нужно писать ни один из них самостоятельно.

person agf    schedule 02.08.2011
comment
+1 Просто хочу отметить, что для всех, кто ищет, этот декоратор доступен как functools.total_ordering. - person Neil G; 02.08.2011
comment
@Lennart: Пожалуйста, добавьте ответ. Я заметил этот миксин. Почему его не добавили в стандартную библиотеку Python, как декоратор? - person Neil G; 02.08.2011
comment
@Neil G: Ну, это не старо и достаточно широко используется. - person Lennart Regebro; 02.08.2011
comment
Я постараюсь не забыть отметить это в опечатках. - person Lennart Regebro; 03.08.2011
comment
Это было исправлено в 3.4, и это, вероятно, лучший ответ. - person Neil G; 19.03.2020
comment
В документах для total_ordering сказано, что вы также должны реализовать __eq__. - person Entropic Thunder; 09.05.2021
comment
@EntropicThunder Добавлено в ответ. - person agf; 10.05.2021

Для полного набора функций сравнения я использовал следующий миксин, который вы можете поместить, например, в свой модуль, например, в файл mixin.py.

class ComparableMixin(object):
    def _compare(self, other, method):
        try:
            return method(self._cmpkey(), other._cmpkey())
        except (AttributeError, TypeError):
            # _cmpkey not implemented, or return different type,
            # so I can't compare with "other".
            return NotImplemented

    def __lt__(self, other):
        return self._compare(other, lambda s, o: s < o)

    def __le__(self, other):
        return self._compare(other, lambda s, o: s <= o)

    def __eq__(self, other):
        return self._compare(other, lambda s, o: s == o)

    def __ge__(self, other):
        return self._compare(other, lambda s, o: s >= o)

    def __gt__(self, other):
        return self._compare(other, lambda s, o: s > o)

    def __ne__(self, other):
        return self._compare(other, lambda s, o: s != o)

Чтобы использовать приведенный выше миксин, вам нужно реализовать метод _cmpkey(), который возвращает ключ объектов, которые можно сравнить, аналогично функции key(), используемой при сортировке. Реализация может выглядеть так:

>>> from .mixin import ComparableMixin

>>> class Orderable(ComparableMixin):
...
...     def __init__(self, firstname, lastname):
...         self.first = firstname
...         self.last = lastname
...
...     def _cmpkey(self):
...         return (self.last, self.first)
...
...     def __repr__(self):
...         return "%s %s" % (self.first, self.last)
...
>>> sorted([Orderable('Donald', 'Duck'), 
...         Orderable('Paul', 'Anka')])
[Paul Anka, Donald Duck]

Причина, по которой я использую это вместо рецепта total_ordering, заключается в этой ошибке. Это исправлено в Python 3.4, но часто вам нужно поддерживать и более старые версии Python.

person Lennart Regebro    schedule 02.08.2011
comment
Стоит указать всем, кто найдет этот ответ, что модуль mixin является гипотетическим модулем, который можно определить — его нет в стандартной библиотеке Python. - person Neil G; 02.08.2011
comment
Теперь ошибка исправлена. Не могли бы вы обновить свой ответ или добавить новый? - person Neil G; 21.05.2015
comment
Благодарю вас! Итак, в Python › 3.4 вы бы рекомендовали применить декоратор functools.total_ordering? - person Neil G; 21.05.2015
comment
Я не смотрел окончательное решение, которое они добавили в 3.4, поэтому не могу сказать наверняка, но, вероятно, оно нормальное. - person Lennart Regebro; 21.05.2015

Не уверен, что это завершено, но вы хотели бы определить:

__eq__, __gt__, __ge__, __lt__, __le__

Как сказал agf, мне не хватает:

__ne__
person jcomeau_ictx    schedule 02.08.2011
comment
Нет ли способа автоматически определить большинство из них с помощью миксина? - person Neil G; 02.08.2011
comment
@Neil: И как бы миксин их определил? - person Ignacio Vazquez-Abrams; 02.08.2011
comment
Смотрите рецепт декоратора в моем ответе. Кроме того, другое сравнение __ne__, не равно. - person agf; 02.08.2011
comment
@Ignacio: Возможно, если один из них определен, он может использовать его для определения остальных? - person Neil G; 02.08.2011
comment
никогда не использовал миксины, извините, ничем не могу помочь. Я все еще немного не уверен в себе с Python3, в основном продолжая использовать Python2. - person jcomeau_ictx; 02.08.2011

Вы сказали, что пытаетесь сделать это:

max((f(obj), obj) for obj in obj_list)[1]

Вы должны просто сделать это:

max(f(obj) for obj in obj_list)

РЕДАКТИРОВАТЬ: Или, как сказал гнибблер: max(obj_list, key=f)

Но вы сказали gnibbler, что вам нужна ссылка на объект max. Я думаю, что это самое простое:

def max_obj(obj_list, max_fn):
    if not obj_list:
        return None

    obj_max = obj_list[0]
    f_max = max_fn(obj)

    for obj in obj_list[1:]:
        if max_fn(obj) > f_max:
            obj_max = obj
    return obj_max

obj = max_obj(obj_list)

Конечно, вы можете позволить ему вызвать исключение, а не возвращать ничего, если вы попытаетесь найти max_obj() пустого списка.

person steveha    schedule 02.08.2011
comment
Первая версия разрывает связь с самим объектом. - person agf; 02.08.2011
comment
Да, я не совсем понял, что вы делали, когда писал первую версию. Я думаю, что он у меня есть сейчас. - person steveha; 02.08.2011
comment
Вам действительно нужно было опубликовать два отдельных ответа? Они оба делают одно и то же, и оба менее ясны, чем оригинал max((f(obj), obj) for obj in obj_list) - person agf; 02.08.2011
comment
Считаете ли вы своего рода нарушением этикета размещение двух совершенно разных ответов как двух отдельных ответов? И вы действительно думаете, что они делают то же самое? Один вызывает max() с ключом, другой — цикл for; как они делают одно и то же? И я не уверен, что согласен, что мои решения менее ясны. Кстати, вы убрали [1] из его ответа, что было достаточно сложно, чтобы обмануть меня в первый раз, когда я его прочитал. - person steveha; 02.08.2011

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

max(((f(obj), obj) for obj in obj_list), key=lambda x: x[0])[1]

Мне это действительно не нравится, поэтому вот что-то менее краткое, что делает то же самое:

def make_pair(f, obj):
    return (f(obj), obj)

def gen_pairs(f, obj_list):
    return (make_pair(f, obj) for obj in obj_list)

def item0(tup):
    return tup[0]

def max_obj(f, obj_list):
    pair = max(gen_pairs(f, obj_list), key=item0)
    return pair[1]

Или вы можете использовать эту однострочную строку, если obj_list всегда является индексируемым объектом, таким как список:

obj_list[max((f(obj), i) for i, obj in enumerate(obj_list))[1]]

Это имеет то преимущество, что если есть несколько объектов, таких, что f(obj) возвращает идентичное значение, вы знаете, какой из них вы получите: тот, у которого самый высокий индекс, то есть самый последний в списке. Если вам нужен самый ранний из списка, вы можете сделать это с помощью ключевой функции.

person steveha    schedule 02.08.2011