¿Cómo ordenar una lista de objetos en función de un atributo de los objetos?

804

Tengo una lista de objetos de Python que me gustaría ordenar por un atributo de los propios objetos. La lista se ve así:

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

Cada objeto tiene un recuento:

>>> ut[1].count
1L

Necesito ordenar la lista por número de conteos descendentes.

He visto varios métodos para esto, pero estoy buscando las mejores prácticas en Python.

Nick Sergeant
fuente
1
Cómo ordenar para aquellos que buscan más información sobre cómo ordenar en Python.
Jeyekomon
1
aparte de operator.attrgetter ('attribute_name') también puede usar functores como clave como object_list.sort (key = my_sorting_functor ('my_key')), dejando la implementación intencionalmente fuera.
vijay shanker

Respuestas:

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)

Más sobre la clasificación por teclas .

Tríptico
fuente
1
No hay problema. por cierto, si muhuk tiene razón y es una lista de objetos Django, debería considerar su solución. Sin embargo, para el caso general de ordenar objetos, mi solución es probablemente la mejor práctica.
Tríptico
43
En listas grandes, obtendrá un mejor rendimiento utilizando operator.attrgetter ('count') como clave. Esta es solo una forma optimizada (nivel inferior) de la función lambda en esta respuesta.
David Eyk
44
Gracias por la gran respuesta. En caso de que sea una lista de diccionarios y 'contar' sea una de sus claves, entonces debe cambiarse de la siguiente manera: ut.sort (key = lambda x: x ['count'], reverse = True)
dganesh2002
Supongo que merece la siguiente actualización: si es necesario ordenar por múltiples campos, podría lograrse mediante llamadas consecutivas a sort (), porque python está utilizando un algoritmo de ordenación estable.
zzz777
86

Una forma que puede ser más rápida, especialmente si su lista tiene muchos registros, es usarla operator.attrgetter("count"). Sin embargo, esto podría ejecutarse en una versión previa al operador de Python, por lo que sería bueno tener un mecanismo de reserva. Es posible que desee hacer lo siguiente, entonces:

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
fuente
77
Aquí usaría el nombre de variable "keyfun" en lugar de "cmpfun" para evitar confusiones. El método sort () también acepta una función de comparación a través del argumento cmp =.
akaihola
Esto no parece funcionar si el objeto ha agregado atributos dinámicamente (si lo ha hecho self.__dict__ = {'some':'dict'}después del __init__método). Sin embargo, no sé por qué podría ser diferente.
tutuca
@tutuca: nunca he reemplazado la instancia __dict__. Tenga en cuenta que "un objeto que tiene atributos agregados dinámicamente" y "establecer el __dict__atributo de un objeto " son conceptos casi ortogonales. Lo digo porque su comentario parece implicar que establecer el __dict__atributo es un requisito para agregar atributos dinámicamente.
tzot
@tzot: Estoy mirando esto: github.com/stochastic-technologies/goatfish/blob/master/… y usando ese iterador aquí: github.com/TallerTechnologies/dishey/blob/master/app.py#L28 plantea error de atributo Tal vez por python3, pero aún así ...
tutuca
1
@tzot: si entiendo el uso de operator.attrgetter, podría proporcionar una función con cualquier nombre de propiedad y devolver una colección ordenada.
Resumen de
64

Los lectores deben notar que la clave = método:

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

es muchas veces más rápido que agregar operadores de comparación enriquecidos a los objetos. Me sorprendió leer esto (página 485 de "Python in a Nutshell"). Puede confirmar esto ejecutando pruebas en este pequeño programa:

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

Mis pruebas, muy mínimas, muestran que el primer tipo es más de 10 veces más lento, pero el libro dice que en general es solo 5 veces más lento. La razón que dicen se debe al algoritmo de clasificación altamente optimizado utilizado en python ( timsort ).

Aún así, es muy extraño que .sort (lambda) sea más rápido que el antiguo .sort (). Espero que arreglen eso.

Jose M Vidal
fuente
1
Definir __cmp__es equivalente a llamar .sort(cmp=lambda), no .sort(key=lambda), por lo que no es extraño en absoluto.
tzot
@tzot tiene toda la razón. El primer tipo tiene que comparar objetos uno contra el otro una y otra vez. La segunda ordenación accede a cada objeto solo una vez para extraer su valor de conteo, y luego realiza una ordenación numérica simple que está altamente optimizada. Una comparación más justa sería longList2.sort(cmp = cmp). Probé esto y funcionó casi igual que .sort(). (También: tenga en cuenta que el parámetro de ordenación "cmp" se eliminó en Python 3.)
Bryan Roach
43

Enfoque orientado a objetos

Es una buena práctica hacer que la lógica de clasificación de objetos, si corresponde, sea una propiedad de la clase en lugar de incorporarse en cada caso en el que se requiere el orden.

Esto garantiza la coherencia y elimina la necesidad de código repetitivo.

Como mínimo, debe especificar __eq__y las __lt__operaciones para que esto funcione. Entonces solo úsalo 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
fuente
1
Eso es lo que estaba buscando! ¿Podría indicarnos alguna documentación que explique por qué __eq__y cuáles __lt__son los requisitos mínimos de implementación?
FriendFX
1
@FriendFX, creo que está implícito en esto :•The sort routines are guaranteed to use __lt__() when making comparisons between two objects...
jpp
2
@FriendFX: Ver portingguide.readthedocs.io/en/latest/comparisons.html para Comparación y Clasificación
Cornel Masson
37
from operator import attrgetter
ut.sort(key = attrgetter('count'), reverse = True)

fuente
16

Se parece mucho a una lista de instancias de modelo ORM de Django.

¿Por qué no ordenarlos en una consulta como esta?

ut = Tag.objects.order_by('-count')
muhuk
fuente
Lo es, pero usando django-tagging, así que estaba usando un incorporado para tomar un conjunto de etiquetas por uso para un conjunto de consultas en particular, así: Tag.objects.usage_for_queryset (QuerySet, count = True)
Nick Sergeant
11

Agregue operadores de comparación enriquecidos a la clase de objeto, luego use el método sort () de la lista.
Vea una rica comparación en python .


Actualización : aunque este método funcionaría, creo que la solución de Triptych se adapta mejor a su caso porque es mucho más simple.

robar
fuente
3

Si el atributo por el que desea ordenar es una propiedad , puede evitar importar operator.attrgettery utilizar el fgetmétodo de la propiedad .

Por ejemplo, para una clase Circlecon una propiedad radiuspodríamos ordenar una lista circlespor radios de la siguiente manera:

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

Esta no es la característica más conocida, pero a menudo me ahorra una línea con la importación.

Georgy
fuente