Comment trier une liste d'objets en fonction d'un attribut des objets?

804

J'ai une liste d'objets Python que j'aimerais trier par attribut des objets eux-mêmes. La liste ressemble à:

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

Chaque objet a un décompte:

>>> ut[1].count
1L

J'ai besoin de trier la liste par nombre de décomptes décroissants.

J'ai vu plusieurs méthodes pour cela, mais je recherche les meilleures pratiques en Python.

Nick Sergeant
la source
1
Comment trier pour ceux qui recherchent plus d'informations sur le tri en Python.
Jeyekomon
1
en dehors de operator.attrgetter ('attribute_name'), vous pouvez également utiliser des foncteurs comme clé comme object_list.sort (key = my_sorting_functor ('my_key')), en laissant l'implémentation intentionnellement.
vijay shanker

Réponses:

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

Plus d'informations sur le tri par clés .

Triptyque
la source
1
Aucun problème. btw, si muhuk a raison et que c'est une liste d'objets Django, vous devriez considérer sa solution. Cependant, pour le cas général du tri des objets, ma solution est probablement la meilleure pratique.
Triptyque
43
Sur de grandes listes, vous obtiendrez de meilleures performances en utilisant operator.attrgetter ('count') comme clé. Ceci est juste une forme optimisée (niveau inférieur) de la fonction lambda dans cette réponse.
David Eyk
4
Merci pour la bonne réponse. Dans le cas où s'il s'agit d'une liste de dictionnaires et que 'count' est l'une de ses clés, il doit être modifié comme ci-dessous: ut.sort (key = lambda x: x ['count'], reverse = True)
dganesh2002
Je suppose que cela mérite la mise à jour suivante: s'il est nécessaire de trier par plusieurs champs, cela pourrait être réalisé par des appels consécutifs à sort (), car python utilise un algorithme de tri stable.
zzz777
86

Un moyen qui peut être plus rapide, surtout si votre liste contient beaucoup d'enregistrements, est d'utiliser operator.attrgetter("count"). Cependant, cela pourrait fonctionner sur une version pré-opérateur de Python, donc ce serait bien d'avoir un mécanisme de secours. Vous pouvez alors faire ce qui suit:

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
la source
7
Ici, j'utiliserais le nom de variable "keyfun" au lieu de "cmpfun" pour éviter toute confusion. La méthode sort () accepte également une fonction de comparaison via l'argument cmp =.
akaihola
Cela ne semble pas fonctionner si l'objet a des attributs ajoutés dynamiquement (si vous l'avez fait self.__dict__ = {'some':'dict'}après la __init__méthode). Je ne sais pas pourquoi ça devrait être différent, cependant.
tutuca
@tutuca: Je n'ai jamais remplacé l'instance __dict__. Notez que "un objet auquel des attributs ont été ajoutés dynamiquement" et "définir l' __dict__attribut d' un objet " sont des concepts presque orthogonaux. Je dis cela parce que votre commentaire semble impliquer que la définition de l' __dict__attribut est une exigence pour l'ajout dynamique d'attributs.
tzot
@tzot: Je regarde bien ceci: github.com/stochastic-technologies/goatfish/blob/master/… et j'utilise cet itérateur ici: github.com/TallerTechnologies/dishey/blob/master/app.py#L28 lève erreur d'attribut. Peut-être à cause de python3, mais quand même ...
tutuca
1
@tzot: si je comprends bien l'utilisation de operator.attrgetter, je pourrais fournir une fonction avec n'importe quel nom de propriété et renvoyer une collection triée.
IAbstract
64

Les lecteurs doivent noter que la méthode key =:

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

est beaucoup plus rapide que l'ajout d'opérateurs de comparaison riches aux objets. J'ai été surpris de lire ceci (page 485 de "Python en bref"). Vous pouvez le confirmer en exécutant des tests sur ce petit programme:

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

Mes tests, très minimes, montrent que le premier tri est plus de 10 fois plus lent, mais le livre dit qu'il n'est que 5 fois plus lent en général. La raison pour laquelle ils disent est due à l'algorithme de tri hautement optimisé utilisé en python ( timsort ).

Pourtant, il est très étrange que .sort (lambda) soit plus rapide que l'ancien .sort (). J'espère qu'ils corrigent cela.

Jose M Vidal
la source
1
Définir __cmp__équivaut à appeler .sort(cmp=lambda), non .sort(key=lambda), donc ce n'est pas étrange du tout.
tzot
@tzot a parfaitement raison. Le premier tri doit comparer les objets les uns aux autres encore et encore. Le deuxième tri accède à chaque objet une seule fois pour extraire sa valeur de comptage, puis il effectue un tri numérique simple qui est hautement optimisé. Une comparaison plus juste serait longList2.sort(cmp = cmp). J'ai essayé cela et cela a fonctionné presque comme .sort(). (Aussi: notez que le paramètre de tri "cmp" a été supprimé dans Python 3.)
Bryan Roach
43

Approche orientée objet

Il est recommandé de faire de la logique de tri des objets, le cas échéant, une propriété de la classe plutôt qu'incorporée dans chaque instance, le classement est requis.

Cela garantit la cohérence et élimine le besoin de code passe-partout.

Au minimum, vous devez spécifier __eq__et les __lt__opérations pour que cela fonctionne. Ensuite, utilisez simplement 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
la source
1
Voilà ce que je cherchais! Pourriez-vous nous indiquer une documentation qui explique pourquoi __eq__et quelles __lt__sont les exigences minimales de mise en œuvre?
FriendFX
1
@FriendFX, je crois qu'il est sous - entendu par ce :•The sort routines are guaranteed to use __lt__() when making comparisons between two objects...
jpp
2
@FriendFX: Voir portingguide.readthedocs.io/en/latest/comparisons.html pour la comparaison et le tri
Cornel Masson
37
from operator import attrgetter
ut.sort(key = attrgetter('count'), reverse = True)

la source
16

Cela ressemble beaucoup à une liste d'instances de modèle Django ORM.

Pourquoi ne pas les trier sur une requête comme celle-ci:

ut = Tag.objects.order_by('-count')
muhuk
la source
C'est le cas, mais en utilisant le django-tagging, donc j'utilisais un intégré pour récupérer un ensemble de balises par utilisation pour un ensemble de requêtes particulier, comme ceci: Tag.objects.usage_for_queryset (QuerySet, count = True)
Nick Sergeant
11

Ajoutez des opérateurs de comparaison riches à la classe d'objets, puis utilisez la méthode sort () de la liste.
Voir une comparaison riche en python .


Mise à jour : Bien que cette méthode fonctionne, je pense que la solution de Triptych est mieux adaptée à votre cas car beaucoup plus simple.

Rob
la source
3

Si l'attribut que vous souhaitez trier est une propriété , vous pouvez éviter d'importer operator.attrgetteret utiliser la fgetméthode de la propriété à la place.

Par exemple, pour une classe Circleavec une propriété, radiusnous pourrions trier une liste de circlesrayons comme suit:

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

Ce n'est pas la fonctionnalité la plus connue, mais cela me sauve souvent une ligne avec l'importation.

Georgy
la source