Posortować listę według wielu atrybutów?

455

Mam listę list:

[[12, 'tall', 'blue', 1],
[2, 'short', 'red', 9],
[4, 'tall', 'blue', 13]]

Gdybym chciał posortować według jednego elementu, powiedzmy wysoki / krótki element, mógłbym to zrobić za pośrednictwem s = sorted(s, key = itemgetter(1)).

Gdybym chciał, aby posortować według zarówno wysoki / krótki i koloru, co mogłem zrobić to rodzaj dwa razy, raz dla każdego elementu, ale jest jakiś szybszy sposób?

bół głowy
źródło
8
Jeśli używasz krotek zamiast list, kolejność python sortuje według wpisów od lewej do prawej podczas uruchamiania sort. Oznacza to, że sorted([(4, 2), (0, 3), (0, 1)]) == [(0, 1), (0, 3), (4, 2)].
Mateen Ulhaq

Odpowiedzi:

771

Klawisz może być funkcją zwracającą krotkę:

s = sorted(s, key = lambda x: (x[1], x[2]))

Lub możesz osiągnąć to samo, używając itemgetter(co jest szybsze i pozwala uniknąć wywołania funkcji Python):

import operator
s = sorted(s, key = operator.itemgetter(1, 2))

I zauważ, że tutaj możesz użyć sortzamiast używać, sorteda następnie ponownie przypisywać:

s.sort(key = operator.itemgetter(1, 2))
Mark Byers
źródło
20
Dla kompletności od czasu: dla mnie najpierw dał 6 us na pętlę, a drugi 4.4 us na pętlę
Brian Larsen
10
Czy istnieje sposób na posortowanie pierwszego rosnąco, a drugiego malejącego? (Załóżmy, że oba atrybuty są ciągami znaków, więc żadne hacki, takie jak dodawanie -liczb całkowitych)
Martin Thoma,
73
co powiesz na to, że jeśli chcę złożyć wniosek revrse=Truetylko, x[1]czy to możliwe?
Amyth
28
@moose, @Amyth, aby odwrócić do tylko jednego atrybutu, możesz posortować dwa razy: najpierw według drugiego, s = sorted(s, key = operator.itemgetter(2))a następnie według pierwotnego s = sorted(s, key = operator.itemgetter(1), reverse=True)Nie idealny, ale działa.
tomcounsell
52
@Amyt lub inna opcja, jeśli klucz jest liczbą, aby odwrócić, możesz go pomnożyć -1.
Serge
37

Nie jestem pewien, czy jest to metoda najbardziej pythońska ... Miałem listę krotek, które wymagały sortowania 1. poprzez zejście liczb całkowitych i 2. alfabetycznie. Wymagało to odwrócenia sortowania według liczb całkowitych, ale nie sortowania alfabetycznego. Oto moje rozwiązanie: (w locie podczas egzaminu btw nawet nie wiedziałem, że można „zagnieżdżać” posortowane funkcje)

a = [('Al', 2),('Bill', 1),('Carol', 2), ('Abel', 3), ('Zeke', 2), ('Chris', 1)]  
b = sorted(sorted(a, key = lambda x : x[0]), key = lambda x : x[1], reverse = True)  
print(b)  
[('Abel', 3), ('Al', 2), ('Carol', 2), ('Zeke', 2), ('Bill', 1), ('Chris', 1)]
Clint Blatchford
źródło
12
ponieważ 2. jest liczbą, działa to tak, aby b = sorted(a, key = lambda x: (-x[1], x[0]))było lepiej widoczne, które kryteria mają zastosowanie jako pierwsze. co do wydajności, nie jestem pewien, ktoś potrzebuje czasu.
Andrei-Niculae Petre
4

Wygląda na to, że możesz użyć listzamiast zamiast tuple. Myślę, że staje się to ważniejsze, gdy chwytasz atrybuty zamiast „magicznych indeksów” listy / krotki.

W moim przypadku chciałem posortować według wielu atrybutów klasy, gdzie przychodzące klucze były łańcuchami. Potrzebowałem innego sortowania w różnych miejscach i chciałem wspólnego domyślnego sortowania dla klasy nadrzędnej, z którą współpracowali klienci; tylko nadpisywanie „kluczy sortujących”, kiedy naprawdę „muszę”, ale także w taki sposób, że mógłbym przechowywać je jako listy, które klasa mogłaby udostępnić

Najpierw zdefiniowałem metodę pomocniczą

def attr_sort(self, attrs=['someAttributeString']:
  '''helper to sort by the attributes named by strings of attrs in order'''
  return lambda k: [ getattr(k, attr) for attr in attrs ]

następnie go użyć

# would defined elsewhere but showing here for consiseness
self.SortListA = ['attrA', 'attrB']
self.SortListB = ['attrC', 'attrA']
records = .... #list of my objects to sort
records.sort(key=self.attr_sort(attrs=self.SortListA))
# perhaps later nearby or in another function
more_records = .... #another list
more_records.sort(key=self.attr_sort(attrs=self.SortListB))

Spowoduje to użycie wygenerowanej funkcji lambda do sortowania listy, object.attrAa następnie object.attrBprzy założeniu, że objectma getter odpowiadający podanym nazwom ciągów. A druga sprawa będzie sortować według object.attrCpotem object.attrA.

Pozwala to również potencjalnie ujawnić opcje sortowania na zewnątrz, które mogą być udostępniane przez konsumenta, test jednostkowy, lub może powiedzieć im, w jaki sposób chcą sortowania dla niektórych operacji w interfejsie API, wystarczy podać listę, a nie łącząc je z implementacją zaplecza.

UpAndAdam
źródło
Dobra robota. Co jeśli atrybuty powinny być sortowane w różnych porządkach? Załóżmy, że attrA powinno być sortowane rosnąco, a attrB malejąco? Czy jest na to szybkie rozwiązanie? Dzięki!
mhn_namak
3

Kilka lat późno do partii, ale chcę zarówno sortowania na 2 kryteriach i użytkowania reverse=True. Jeśli ktoś chce wiedzieć, jak to zrobić, możesz zawrzeć kryteria (funkcje) w nawiasach:

s = sorted(my_list, key=lambda i: ( criteria_1(i), criteria_2(i) ), reverse=True)
donrondadon
źródło
1

Oto jeden sposób: Zasadniczo ponownie piszesz swoją funkcję sortowania, aby pobrać listę funkcji sortowania, każda funkcja sortowania porównuje atrybuty, które chcesz przetestować, w każdym teście sortowania patrzysz i sprawdzasz, czy funkcja cmp zwraca niezerowy zwrot jeśli tak, przerwij i wyślij wartość zwrotną. Wywołujecie to, wywołując Lambda funkcji listy Lambdas.

Jego zaletą jest to, że przesyła dane pojedynczo, a nie jak poprzednie sortowanie, jak robią to inne metody. Kolejną rzeczą jest to, że sortuje się w miejscu, podczas gdy sortowanie wydaje się robić kopię.

Użyłem go do napisania funkcji rankingu, która uszeregowuje listę klas, w których każdy obiekt jest w grupie i ma funkcję punktacji, ale możesz dodać dowolną listę atrybutów. Zwróć uwagę na nie-lambda, choć hackerskie użycie lambda do wezwania setera. Część rangowa nie będzie działać dla szeregu list, ale sortowanie będzie.

#First, here's  a pure list version
my_sortLambdaLst = [lambda x,y:cmp(x[0], y[0]), lambda x,y:cmp(x[1], y[1])]
def multi_attribute_sort(x,y):
    r = 0
    for l in my_sortLambdaLst:
        r = l(x,y)
        if r!=0: return r #keep looping till you see a difference
    return r

Lst = [(4, 2.0), (4, 0.01), (4, 0.9), (4, 0.999),(4, 0.2), (1, 2.0), (1, 0.01), (1, 0.9), (1, 0.999), (1, 0.2) ]
Lst.sort(lambda x,y:multi_attribute_sort(x,y)) #The Lambda of the Lambda
for rec in Lst: print str(rec)

Oto sposób na uszeregowanie listy obiektów

class probe:
    def __init__(self, group, score):
        self.group = group
        self.score = score
        self.rank =-1
    def set_rank(self, r):
        self.rank = r
    def __str__(self):
        return '\t'.join([str(self.group), str(self.score), str(self.rank)]) 


def RankLst(inLst, group_lambda= lambda x:x.group, sortLambdaLst = [lambda x,y:cmp(x.group, y.group), lambda x,y:cmp(x.score, y.score)], SetRank_Lambda = lambda x, rank:x.set_rank(rank)):
    #Inner function is the only way (I could think of) to pass the sortLambdaLst into a sort function
    def multi_attribute_sort(x,y):
        r = 0
        for l in sortLambdaLst:
            r = l(x,y)
            if r!=0: return r #keep looping till you see a difference
        return r

    inLst.sort(lambda x,y:multi_attribute_sort(x,y))
    #Now Rank your probes
    rank = 0
    last_group = group_lambda(inLst[0])
    for i in range(len(inLst)):
        rec = inLst[i]
        group = group_lambda(rec)
        if last_group == group: 
            rank+=1
        else:
            rank=1
            last_group = group
        SetRank_Lambda(inLst[i], rank) #This is pure evil!! The lambda purists are gnashing their teeth

Lst = [probe(4, 2.0), probe(4, 0.01), probe(4, 0.9), probe(4, 0.999), probe(4, 0.2), probe(1, 2.0), probe(1, 0.01), probe(1, 0.9), probe(1, 0.999), probe(1, 0.2) ]

RankLst(Lst, group_lambda= lambda x:x.group, sortLambdaLst = [lambda x,y:cmp(x.group, y.group), lambda x,y:cmp(x.score, y.score)], SetRank_Lambda = lambda x, rank:x.set_rank(rank))
print '\t'.join(['group', 'score', 'rank']) 
for r in Lst: print r
Dominic Suciu
źródło