Classificar uma lista por vários atributos?

457

Eu tenho uma lista de listas:

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

Se eu quisesse classificar por um elemento, digamos o elemento alto / baixo, eu poderia fazê-lo via s = sorted(s, key = itemgetter(1)).

Se eu quisesse classificar por tanto alto / baixo e cor, eu poderia fazer o tipo duas vezes, uma para cada elemento, mas há uma maneira mais rápida?

dor de cabeça
fonte
8
Se você usar tuplas em vez de listas, as ordens do python serão classificadas por entradas da esquerda para a direita quando você executar sort. Isto é sorted([(4, 2), (0, 3), (0, 1)]) == [(0, 1), (0, 3), (4, 2)].
Mateen Ulhaq

Respostas:

772

Uma chave pode ser uma função que retorna uma tupla:

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

Ou você pode conseguir o mesmo usando itemgetter(o que é mais rápido e evita uma chamada de função Python):

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

E observe que aqui você pode usar em sortvez de usar sortede depois redesignar:

s.sort(key = operator.itemgetter(1, 2))
Mark Byers
fonte
20
Para completar a partir timeit: para mim primeiro deu 6-nos por ciclo eo segundo 4,4 nós por laço
Brian Larsen
10
Existe uma maneira de classificar o primeiro ascendente e o segundo descendente? (Suponha que os dois atributos são strings, então há hacks como a adição -de números inteiros)
Martin Thoma
73
Que tal se eu quiser me candidatar revrse=Trueapenas a x[1]isso é possível?
Amyth
28
@moose, @Amyth, para reverter para apenas um atributo, você pode classificar duas vezes: primeiro pelo secundário e s = sorted(s, key = operator.itemgetter(2))depois pelo primário s = sorted(s, key = operator.itemgetter(1), reverse=True)Não é o ideal, mas funciona.
precisa saber é o seguinte
52
@Amyth ou outra opção, se a chave for número, para fazer a inversão, você pode multiplicá-la -1.
Serge
37

Não tenho certeza se esse é o método mais pitonico ... Eu tinha uma lista de tuplas que precisavam classificar primeiro por valores inteiros decrescentes e segundo alfabeticamente. Isso exigia a reversão da classificação inteira, mas não da ordem alfabética. Aqui estava a minha solução: (em tempo real, em um exame, eu nem sabia que era possível "aninhar" funções classificadas)

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
fonte
13
como 2º é um número, ele trabalha para fazê-lo como o b = sorted(a, key = lambda x: (-x[1], x[0]))que é mais visível em quais critérios se aplicam primeiro. quanto à eficiência, não tenho certeza, alguém precisa cronometrar.
Andrei-Niculae Petre
5

Parece que você poderia usar a em listvez de a tuple. Isso se torna mais importante quando você pega atributos em vez de 'índices mágicos' de uma lista / tupla.

No meu caso, eu queria classificar por vários atributos de uma classe, onde as chaves recebidas eram cadeias de caracteres. Eu precisava de classificação diferente em lugares diferentes e queria uma classificação padrão comum para a classe pai com a qual os clientes estavam interagindo; apenas tendo que substituir as 'chaves de classificação' quando eu realmente 'precisava', mas também de uma maneira que eu pudesse armazená-las como listas que a classe pudesse compartilhar

Então, primeiro eu defini um 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 ]

então para usá-lo

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

Isso usará a função lambda gerada, classificará a lista object.attrAe, desde então, object.attrBassumirá objectum getter correspondente aos nomes de string fornecidos. E o segundo caso seria resolvido até object.attrCentão object.attrA.

Isso também permite que você exponha potencialmente as opções de classificação externa a serem compartilhadas da mesma forma por um consumidor, um teste de unidade ou para que talvez lhe digam como desejam que a classificação seja feita para alguma operação em sua API, apenas para fornecer uma lista e não acoplando-os à sua implementação de back-end.

UpAndAdam
fonte
Bom trabalho. E se os atributos forem classificados em ordens diferentes? Suponha que attrA deve ser classificado em ordem crescente e attrB em ordem decrescente? Existe uma solução rápida em cima disso? Obrigado!
mhn_namak 03/02
4

Vários anos atrasado para a festa, mas eu quero tanto espécie em 2 critérios e uso reverse=True. Caso alguém queira saber como, você pode colocar seus critérios (funções) entre parênteses:

s = sorted(my_list, key=lambda i: ( criteria_1(i), criteria_2(i) ), reverse=True)
Donrondadon
fonte
1

Aqui está uma maneira: Você basicamente reescreve sua função de classificação para obter uma lista de funções de classificação, cada função de classificação compara os atributos que você deseja testar, em cada teste de classificação, você olha e vê se a função cmp retorna um retorno diferente de zero nesse caso, interrompa e envie o valor de retorno. Você o chama chamando um Lambda de uma função de uma lista de Lambdas.

Sua vantagem é que ele passa os dados de maneira única, não como uma espécie anterior, como outros métodos. Outra coisa é que ele é ordenado, enquanto que o ordenado parece fazer uma cópia.

Usei-o para escrever uma função de classificação, que classifica uma lista de classes em que cada objeto está em um grupo e tem uma função de pontuação, mas você pode adicionar qualquer lista de atributos. Observe o uso não lambda, embora hackish, de um lambda para chamar um setter. A parte de classificação não funcionará para uma matriz de listas, mas a classificação funcionará.

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

Aqui está uma maneira de classificar uma 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
fonte