Jak posortować listę obiektów na podstawie atrybutu obiektów?

803

Mam listę obiektów Python, które chciałbym posortować według atrybutu samych obiektów. Lista wygląda następująco:

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

Każdy obiekt ma liczbę:

>>> ut[1].count
1L

Muszę posortować listę według liczby malejących.

Widziałem kilka metod, ale szukam najlepszych praktyk w Pythonie.

Nick Sierżant
źródło
1
Sortowanie JAK dla tych, którzy szukają więcej informacji o sortowaniu w Pythonie.
Jeyekomon
1
oprócz operatora.attrgetter ('nazwa_atrybutu') możesz także używać funktorów jako klucza, np. object_list.sort (key = my_sorting_functor ('my_key')), pomijając implementację celowo.
vijay shanker

Odpowiedzi:

1312
# 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)

Więcej informacji na temat sortowania według kluczy .

Tryptyk
źródło
1
Nie ma problemu. przy okazji, jeśli muhuk ma rację i jest to lista obiektów Django, powinieneś rozważyć jego rozwiązanie. Jednak w ogólnym przypadku sortowania obiektów moje rozwiązanie jest prawdopodobnie najlepszą praktyką.
Tryptyk
43
Na dużych listach uzyskasz lepszą wydajność, używając operatora.attrgetter ('count') jako klucza. W tej odpowiedzi jest to tylko zoptymalizowana (niższy poziom) forma funkcji lambda.
David Eyk,
4
Dzięki za świetną odpowiedź. W przypadku, gdy jest to lista słowników, a 'count' jest jednym z jej kluczy, należy ją zmienić jak poniżej: ut.sort (key = lambda x: x ['count'], reverse = True)
dganesh2002
Przypuszczam, że zasługuje na następującą aktualizację: jeśli istnieje potrzeba sortowania według wielu pól, można to osiągnąć przez kolejne wywołania metody sort (), ponieważ Python używa stabilnego algorytmu sortowania.
zzz777
86

Najlepszym sposobem, który może być najszybszy, szczególnie jeśli twoja lista zawiera wiele rekordów, jest użycie operator.attrgetter("count"). Może to jednak działać na wcześniejszej wersji Pythona, więc dobrze byłoby mieć mechanizm awaryjny. Następnie możesz wykonać następujące czynności:

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
źródło
7
Tutaj użyłbym nazwy zmiennej „keyfun” zamiast „cmpfun”, aby uniknąć nieporozumień. Metoda sort () akceptuje również funkcję porównania za pomocą argumentu cmp =.
akaihola
Wydaje się, że to nie działa, jeśli obiekt ma dynamicznie dodane atrybuty (jeśli zrobiłeś to self.__dict__ = {'some':'dict'}po __init__metodzie). Nie wiem jednak, dlaczego miałoby być inaczej.
tutuca
@tutuca: Nigdy nie zastępowałem tego wystąpienia __dict__. Zauważ, że „obiekt z dynamicznie dodawanymi atrybutami” i „ustawienie __dict__atrybutu obiektu ” są prawie ortogonalnymi pojęciami. Mówię to, ponieważ twój komentarz wydaje się sugerować, że ustawienie __dict__atrybutu jest warunkiem dynamicznego dodawania atrybutów.
tzot
@tzot: Patrzę właśnie na to: github.com/stochastic-technologies/goatfish/blob/master/… i używając tego iteratora tutaj: github.com/TallerTechnologies/dishey/blob/master/app.py#L28 podnosi błąd atrybutu. Może z powodu python3, ale wciąż ...
tutuca,
1
@tzot: jeśli rozumiem użycie operator.attrgetter, mógłbym podać funkcję o dowolnej nazwie właściwości i zwrócić posortowaną kolekcję.
IAbstract
64

Czytelnicy powinni zauważyć, że metoda key =:

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

jest wiele razy szybsza niż dodawanie do obiektów bogatych operatorów porównania. Byłem zaskoczony, gdy to przeczytałem (str. 485 „Python w pigułce”). Możesz to potwierdzić, uruchamiając testy w tym małym programie:

#!/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

Moje, bardzo minimalne testy pokazują, że pierwsze sortowanie jest ponad 10 razy wolniejsze, ale książka mówi, że ogólnie jest tylko około 5 razy wolniejsze. Mówią, że powodem jest wysoce zoptymalizowany algorytm sortowania używany w pythonie ( timsort ).

Jednak bardzo dziwne jest to, że .sort (lambda) jest szybszy niż zwykły stary .sort (). Mam nadzieję, że to naprawią.

Jose M. Vidal
źródło
1
Definiowanie __cmp__jest równoznaczne z dzwonieniem .sort(cmp=lambda), .sort(key=lambda)więc nie jest wcale dziwne.
tzot
@tzot ma dokładnie rację. Pierwszy rodzaj musi ciągle porównywać obiekty. Drugie sortowanie uzyskuje dostęp do każdego obiektu tylko raz, aby wyodrębnić jego wartość zliczania, a następnie wykonuje proste sortowanie numeryczne, które jest wysoce zoptymalizowane. Bardziej sprawiedliwe byłoby porównanie longList2.sort(cmp = cmp). Wypróbowałem to i działało prawie tak samo jak .sort(). (Również: zauważ, że parametr sortowania „cmp” został usunięty w Pythonie 3.)
Bryan Roach
43

Podejście obiektowe

Dobrą praktyką jest, aby logika sortowania obiektów, jeśli ma zastosowanie, była właściwością klasy, a nie włączana w każdym przypadku, w którym wymagane jest porządkowanie.

Zapewnia to spójność i eliminuje potrzebę stosowania kodu płyty kotłowej.

Co najmniej należy określić __eq__i __lt__działania, aby to zadziałało. Więc po prostu użyj 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
źródło
1
Właśnie tego szukałem! Czy możesz wskazać nam dokumentację, która wyjaśnia, dlaczego __eq__i jakie __lt__są minimalne wymagania dotyczące wdrażania?
FriendFX,
1
@FriendFX, uważam, że wynika to z :•The sort routines are guaranteed to use __lt__() when making comparisons between two objects...
jpp
2
@FriendFX: Zobacz portingguide.readthedocs.io/en/latest/comparisons.html w celu porównania i sortowania
Cornel Masson
37
from operator import attrgetter
ut.sort(key = attrgetter('count'), reverse = True)

źródło
16

Wygląda bardzo podobnie do listy instancji modelu Django ORM.

Dlaczego nie posortować ich według zapytania:

ut = Tag.objects.order_by('-count')
muhuk
źródło
Jest, ale używam tagowania django, więc użyłem wbudowanego do pobrania zestawu tagów przez użycie dla określonego zestawu zapytań, na przykład: Tag.objects.usage_for_queryset (QuerySet, counts = True)
Nick Sergeant
11

Dodaj operatory porównania bogatego do klasy obiektowej, a następnie użyj metody sort () z listy.
Zobacz bogate porównanie w pythonie .


Aktualizacja : Chociaż ta metoda zadziałałaby, myślę, że rozwiązanie z Tryptyku lepiej pasuje do twojego przypadku, ponieważ jest prostsze.

obrabować
źródło
3

Jeśli atrybut, który chcesz posortować, jest właściwością , możesz uniknąć importowania operator.attrgetteri fgetzamiast tego użyć metody właściwości .

Na przykład dla klasy Circlez właściwością radiusmożemy posortować listę circleswedług promieni w następujący sposób:

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

To nie jest najbardziej znana funkcja, ale często zapisuje mi linię przy imporcie.

Georgy
źródło