Ordenar una lista por múltiples atributos?

457

Tengo una lista de listas:

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

Si quisiera ordenar por un elemento, digamos el elemento alto / bajo, podría hacerlo a través de s = sorted(s, key = itemgetter(1)).

Si quería para ordenar por tanto alto / bajo y el color, que podría hacer la clase dos veces, una para cada elemento, pero ¿hay una forma más rápida?

dolor de cabeza
fuente
8
Si usa tuplas en lugar de listas, las órdenes de Python se ordenan por entradas de izquierda a derecha cuando ejecuta sort. Es decir, sorted([(4, 2), (0, 3), (0, 1)]) == [(0, 1), (0, 3), (4, 2)].
Mateen Ulhaq

Respuestas:

772

Una tecla puede ser una función que devuelve una tupla:

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

O puede lograr lo mismo usando itemgetter(que es más rápido y evita una llamada a la función Python):

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

Y observe que aquí puede usar en sortlugar de usar sortedy luego reasignar:

s.sort(key = operator.itemgetter(1, 2))
Mark Byers
fuente
20
Para completar desde el primer momento: para mí primero me dio 6 us por ciclo y el segundo 4.4 us por ciclo
Brian Larsen
10
¿Hay alguna manera de ordenar el primero ascendente y el segundo descendente? (Suponga que ambos atributos son cadenas, por lo que no hay trucos como agregar -para enteros)
Martin Thoma
73
¿qué tal si quiero postular revrse=Truesolo a x[1]eso es posible?
Amyth
28
@moose, @Amyth, para revertir a un solo atributo, puede ordenar dos veces: primero por el secundario y s = sorted(s, key = operator.itemgetter(2))luego por el primario s = sorted(s, key = operator.itemgetter(1), reverse=True)No es ideal, pero funciona.
Tomcounsell
52
@Amyth u otra opción, si la clave es número, para revertirla, puede multiplicarla por -1.
Serge
37

No estoy seguro de si este es el método más pitónico ... Tenía una lista de tuplas que necesitaba clasificarse primero por valores enteros descendentes y segundo alfabéticamente. Esto requería invertir el orden entero pero no el orden alfabético. Aquí estaba mi solución: (sobre la marcha en un examen, por cierto, ni siquiera sabía que podía 'anidar' funciones ordenadas)

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
fuente
13
dado que 2nd es un número, funciona para hacerlo como b = sorted(a, key = lambda x: (-x[1], x[0]))cuál es más visible en qué criterio se aplica primero. En cuanto a la eficiencia, no estoy seguro, alguien necesita tiempo.
Andrei-Niculae Petre
5

Varios años más tarde a la fiesta pero quiero tanto especie en 2 criterios y uso reverse=True. En caso de que alguien más quiera saber cómo, puede ajustar sus criterios (funciones) entre paréntesis:

s = sorted(my_list, key=lambda i: ( criteria_1(i), criteria_2(i) ), reverse=True)
donrondadon
fuente
5

Parece que podrías usar un en listlugar de un tuple. Esto se vuelve más importante, creo, cuando estás tomando atributos en lugar de 'índices mágicos' de una lista / tupla.

En mi caso, quería ordenar por múltiples atributos de una clase, donde las claves entrantes eran cadenas. Necesitaba una clasificación diferente en diferentes lugares, y quería una clasificación predeterminada común para la clase principal con la que los clientes interactuaban; solo tener que anular las 'claves de clasificación' cuando realmente 'lo necesitaba', pero también de una manera que pudiera almacenarlas como listas que la clase podría compartir

Así que primero definí un método auxiliar

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 ]

entonces para usarlo

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

Esto usará la función lambda generada, clasificará la lista object.attrAy luego object.attrBsuponiendo que objecttenga un captador correspondiente a los nombres de cadena proporcionados. Y el segundo caso se resolvería para object.attrCentonces object.attrA.

Esto también le permite exponer potencialmente las opciones de clasificación externas para que un consumidor las comparta por igual, una prueba unitaria, o para que le digan cómo quieren que se realice la clasificación para alguna operación en su API solo tiene que darle una lista y no acoplándolos a su implementación de back-end.

UpAndAdam
fuente
Buen trabajo. ¿Qué pasa si los atributos se deben ordenar en diferentes órdenes? ¿Supongamos que attrA debería clasificarse ascendente y attrB descendente? ¿Hay una solución rápida además de esto? ¡Gracias!
mhn_namak
1

Aquí hay una manera: básicamente reescribe su función de clasificación para tomar una lista de funciones de clasificación, cada función de clasificación compara los atributos que desea probar, en cada prueba de clasificación, observa y ve si la función cmp devuelve un retorno distinto de cero si es así, rompa y envíe el valor de retorno. Lo llamas llamando a un Lambda de una función de una lista de Lambdas.

Su ventaja es que solo pasa a través de los datos, no una especie de clasificación anterior como lo hacen otros métodos. Otra cosa es que se ordena en su lugar, mientras que ordenado parece hacer una copia.

Lo usé para escribir una función de clasificación, que clasifica una lista de clases donde cada objeto está en un grupo y tiene una función de puntuación, pero puede agregar cualquier lista de atributos. Tenga en cuenta el uso no lambda, aunque hack de una lambda para llamar a un setter. La parte de rango no funcionará para una variedad de listas, pero sí lo hará.

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

Aquí hay una manera de clasificar una lista de objetos

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
fuente