Trier une liste par plusieurs attributs?

457

J'ai une liste de listes:

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

Si je voulais trier par un élément, disons l'élément haut / court, je pourrais le faire via s = sorted(s, key = itemgetter(1)).

Si je voulais trier à la fois haut / court et couleur, je pourrais faire le tri deux fois, une fois pour chaque élément, mais existe-t-il un moyen plus rapide?

mal de crâne
la source
8
Si vous utilisez des tuples au lieu de listes, python ordonne le tri par entrées de gauche à droite lorsque vous exécutez sort. C'est sorted([(4, 2), (0, 3), (0, 1)]) == [(0, 1), (0, 3), (4, 2)].
Mateen Ulhaq

Réponses:

772

Une clé peut être une fonction qui renvoie un tuple:

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

Ou vous pouvez obtenir la même chose en utilisant itemgetter(ce qui est plus rapide et évite un appel de fonction Python):

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

Et notez qu'ici, vous pouvez utiliser sortau lieu d'utiliser sortedpuis de réaffecter:

s.sort(key = operator.itemgetter(1, 2))
Mark Byers
la source
20
Pour l'exhaustivité de timeit: pour moi, le premier a donné 6 nous par boucle et le second 4,4 nous par boucle
Brian Larsen
10
Existe-t-il un moyen de trier le premier en ordre croissant et le second en ordre décroissant? (Supposons que les deux attributs sont des chaînes, donc pas de hacks comme l'ajout -de nombres entiers)
Martin Thoma
73
Et si je veux postuler revrse=Trueuniquement, x[1]est-ce possible?
Amyth
28
@moose, @Amyth, pour inverser à un seul attribut, vous pouvez trier deux fois: d'abord par le secondaire s = sorted(s, key = operator.itemgetter(2))puis par le primaire s = sorted(s, key = operator.itemgetter(1), reverse=True)Pas idéal, mais ça marche.
tomcounsell
52
@Amyth ou une autre option, si la clé est un nombre, pour l'inverser, vous pouvez la multiplier par -1.
Serge
37

Je ne suis pas sûr que ce soit la méthode la plus pythonique ... J'avais une liste de tuples qui devaient être triés en premier par ordre décroissant des valeurs entières et ensuite par ordre alphabétique. Cela a nécessité d'inverser le tri entier mais pas le tri alphabétique. Voici ma solution: (à la volée dans un examen btw, je ne savais même pas que vous pouviez «imbriquer» des fonctions triées)

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
la source
13
puisque 2nd est un nombre, il fonctionne pour le faire comme b = sorted(a, key = lambda x: (-x[1], x[0]))ce qui est le plus visible sur quels critères s'applique en premier. quant à l'efficacité, je ne suis pas sûr, quelqu'un doit le chronométrer.
Andrei-Niculae Petre
5

Plusieurs années de retard à la fête mais je souhaite à la fois trier sur 2 critères et utiliser reverse=True. Si quelqu'un d'autre veut savoir comment, vous pouvez mettre vos critères (fonctions) entre parenthèses:

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

Il semble que vous puissiez utiliser un listau lieu d'untuple . Cela devient plus important, je pense, lorsque vous saisissez des attributs au lieu des «index magiques» d'une liste / tuple.

Dans mon cas, je voulais trier par plusieurs attributs d'une classe, où les clés entrantes étaient des chaînes. J'avais besoin d'un tri différent à différents endroits et je voulais un tri par défaut commun pour la classe parent avec laquelle les clients interagissaient; avoir seulement à remplacer les «clés de tri» quand j'en avais vraiment «besoin», mais aussi de manière à pouvoir les stocker sous forme de listes que la classe pourrait partager

J'ai donc d'abord défini une méthode d'aide

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 ]

puis l'utiliser

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

Cela utilisera la fonction lambda générée pour trier la liste object.attrA, puis en object.attrBsupposant qu'elle objectpossède un getter correspondant aux noms de chaîne fournis. Et le deuxième cas trierait d'ici object.attrCobject.attrA .

Cela vous permet également d'exposer potentiellement les choix de tri vers l'extérieur à partager de la même manière par un consommateur, un test unitaire, ou pour qu'ils vous disent peut-être comment ils veulent que le tri soit effectué pour une opération dans votre API en n'ayant qu'à vous donner une liste et non les coupler à votre implémentation back-end.

UpAndAdam
la source
Bon travail. Et si les attributs devaient être triés dans des ordres différents? Supposons que attrA doit être trié en ordre croissant et attrB en ordre décroissant? Y a-t-il une solution rapide en plus de cela? Merci!
mhn_namak
1

Voici une façon: vous réécrivez essentiellement votre fonction de tri pour prendre une liste de fonctions de tri, chaque fonction de tri compare les attributs que vous souhaitez tester, à chaque test de tri, vous regardez et voyez si la fonction cmp renvoie un retour non nul si c'est le cas, rompez et envoyez la valeur de retour. Vous l'appelez en appelant un Lambda d'une fonction d'une liste de Lambdas.

Son avantage est qu'il ne traverse pas les données, pas une sorte de tri précédent comme le font d'autres méthodes. Une autre chose est qu'il trie sur place, alors que trié semble en faire une copie.

Je l'ai utilisé pour écrire une fonction de classement, qui classe une liste de classes où chaque objet est dans un groupe et a une fonction de score, mais vous pouvez ajouter n'importe quelle liste d'attributs. Notez l'utilisation non-lambda-like, mais hackish d'un lambda pour appeler un setter. La partie classement ne fonctionnera pas pour un tableau de listes, mais le tri fonctionnera.

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

Voici un moyen de classer une liste d'objets

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
la source