Как отсортировать список объектов на основе атрибута объектов?

804

У меня есть список объектов Python, которые я хотел бы отсортировать по атрибутам самих объектов. Список выглядит так:

>>> ut
[<Tag: 128>, <Tag: 2008>, <Tag: <>, <Tag: actionscript>, <Tag: addresses>,
 <Tag: aes>, <Tag: ajax> ...]

Каждый объект имеет количество:

>>> ut[1].count
1L

Мне нужно отсортировать список по убыванию количества отсчетов.

Я видел несколько методов для этого, но я ищу лучшую практику в Python.

Ник сержант
источник
1
Сортировка КАК для тех, кто ищет больше информации о сортировке в Python.
Jeyekomon
1
Помимо operator.attrgetter ('attribute_name'), вы также можете использовать функторы в качестве ключа, например object_list.sort (key = my_sorting_functor ('my_key')), оставляя реализацию намеренно.
Виджай Шенкер

Ответы:

1315
# To sort the list in place...
ut.sort(key=lambda x: x.count, reverse=True)

# To return a new list, use the sorted() built-in function...
newlist = sorted(ut, key=lambda x: x.count, reverse=True)

Подробнее о сортировке по ключам .

Триптих
источник
1
Нет проблем. Кстати, если Мухук прав и это список объектов Django, вы должны рассмотреть его решение. Однако для общего случая сортировки объектов моё решение, вероятно, является лучшей практикой.
Триптих
44
В больших списках вы получите лучшую производительность, используя operator.attrgetter ('count') в качестве ключа. Это просто оптимизированная (более низкого уровня) форма лямбда-функции в этом ответе.
Дэвид Эйк
4
Спасибо за отличный ответ. В случае, если это список словарей и «count» является одним из его ключей, его необходимо изменить, как показано ниже: ut.sort (key = lambda x: x ['count'], reverse = True)
dganesh2002
Я полагаю, что это заслуживает следующего обновления: если есть необходимость сортировки по нескольким полям, это может быть достигнуто последовательными вызовами sort (), потому что python использует стабильный алгоритм сортировки.
zzz777
86

Можно использовать самый быстрый способ, особенно если в вашем списке много записей operator.attrgetter("count"). Однако это может выполняться на предоператорной версии Python, поэтому было бы неплохо иметь запасной механизм. Тогда вы можете сделать следующее:

try: import operator
except ImportError: keyfun= lambda x: x.count # use a lambda if no operator module
else: keyfun= operator.attrgetter("count") # use operator since it's faster than lambda

ut.sort(key=keyfun, reverse=True) # sort in-place
tzot
источник
7
Здесь я бы использовал имя переменной «keyfun» вместо «cmpfun», чтобы избежать путаницы. Метод sort () также принимает функцию сравнения через аргумент cmp =.
akaihola
Это не работает, если объект имеет динамически добавленные атрибуты (если вы сделали self.__dict__ = {'some':'dict'}после __init__метода). Я не знаю, почему все может быть иначе.
Тутука
@tutuca: я никогда не заменял экземпляр __dict__. Обратите внимание, что «объект, имеющий динамически добавленные атрибуты» и «установка __dict__атрибута объекта » являются почти ортогональными понятиями. Я говорю это, потому что ваш комментарий подразумевает, что установка __dict__атрибута является обязательным условием для динамического добавления атрибутов.
tzot
@tzot: я смотрю прямо на это: github.com/stochastic-technologies/goatfish/blob/master/… и использую этот итератор здесь: github.com/TallerTechnologies/dishey/blob/master/app.py#L28 поднимает ошибка атрибута. Может быть из-за python3, но все же ...
Тутука
1
@tzot: если я понимаю использование operator.attrgetter, я мог бы предоставить функцию с любым именем свойства и вернуть отсортированную коллекцию.
IAbstract
64

Читатели должны заметить, что ключ = метод:

ut.sort(key=lambda x: x.count, reverse=True)

во много раз быстрее, чем добавление богатых операторов сравнения к объектам. Я был удивлен, прочитав это (страница 485 «Питона в двух словах»). Вы можете подтвердить это, запустив тесты этой маленькой программы:

#!/usr/bin/env python
import random

class C:
    def __init__(self,count):
        self.count = count

    def __cmp__(self,other):
        return cmp(self.count,other.count)

longList = [C(random.random()) for i in xrange(1000000)] #about 6.1 secs
longList2 = longList[:]

longList.sort() #about 52 - 6.1 = 46 secs
longList2.sort(key = lambda c: c.count) #about 9 - 6.1 = 3 secs

Мои, очень минимальные, тесты показывают, что первый сорт более чем в 10 раз медленнее, но в книге говорится, что в целом он примерно в 5 раз медленнее. Причина, по которой они говорят, заключается в высокооптимизируемом алгоритме сортировки, используемом в python ( timsort ).

Тем не менее, очень странно, что .sort (лямбда) быстрее, чем обычный старый .sort (). Я надеюсь, что они исправят это.

Хосе М Видаль
источник
1
Определение __cmp__эквивалентно вызову .sort(cmp=lambda), а не .sort(key=lambda)так, это не странно вообще.
Цот
@tzot совершенно верно. Первый вид должен сравнивать объекты друг с другом снова и снова. Вторая сортировка обращается к каждому объекту только один раз, чтобы извлечь значение счетчика, а затем выполняет простую числовую сортировку, которая высоко оптимизирована. Более справедливое сравнение было бы longList2.sort(cmp = cmp). Я попробовал это, и это почти так же, как и .sort(). (Также: обратите внимание, что параметр сортировки "cmp" был удален в Python 3.)
Брайан Роуч
43

Объектно-ориентированный подход

Хорошей практикой является сделать логику сортировки объектов, если это применимо, свойством класса, а не включать его в каждом случае, когда требуется упорядочение.

Это обеспечивает последовательность и устраняет необходимость в шаблонном коде.

Как минимум, вы должны указать __eq__и __lt__операции для этого, чтобы работать. Тогда просто используйте sorted(list_of_objects).

class Card(object):

    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

    def __eq__(self, other):
        return self.rank == other.rank and self.suit == other.suit

    def __lt__(self, other):
        return self.rank < other.rank

hand = [Card(10, 'H'), Card(2, 'h'), Card(12, 'h'), Card(13, 'h'), Card(14, 'h')]
hand_order = [c.rank for c in hand]  # [10, 2, 12, 13, 14]

hand_sorted = sorted(hand)
hand_sorted_order = [c.rank for c in hand_sorted]  # [2, 10, 12, 13, 14]
JPP
источник
1
Вот что я искал! Не могли бы вы указать нам некоторую документацию, которая объясняет, почему __eq__и __lt__каковы минимальные требования к реализации?
FriendFX
1
@FriendFX, я полагаю, это подразумевается этим :•The sort routines are guaranteed to use __lt__() when making comparisons between two objects...
jpp
2
@FriendFX: см. Portingguide.readthedocs.io/en/latest/comparisons.html для сравнения и сортировки
Корнел Массон
37
from operator import attrgetter
ut.sort(key = attrgetter('count'), reverse = True)

источник
16

Это очень похоже на список экземпляров модели Django ORM.

Почему бы не отсортировать их по запросу так:

ut = Tag.objects.order_by('-count')
muhuk
источник
Это так, но с использованием django-tagging, поэтому я использовал встроенный модуль для захвата тега, установленного с использованием определенного набора запросов, например так: Tag.objects.usage_for_queryset (QuerySet, counts = True)
Ник Сержант
11

Добавьте операторы расширенного сравнения в класс объекта, затем используйте метод sort () из списка.
Смотрите богатое сравнение в Python .


Обновление : хотя этот метод будет работать, я думаю, что решение от Triptych лучше подходит для вашего случая, потому что это намного проще.

грабить
источник
3

Если атрибут, по которому вы хотите отсортировать, является свойством , тогда вы можете избежать импорта operator.attrgetterи использовать fgetвместо этого метод свойства .

Например, для класса Circleсо свойством radiusмы могли бы отсортировать список circlesпо радиусам следующим образом:

result = sorted(circles, key=Circle.radius.fget)

Это не самая известная функция, но она часто экономит мне на импорте.

Георгий
источник