Como classificar uma lista de objetos com base em um atributo dos objetos?

804

Eu tenho uma lista de objetos Python que gostaria de classificar por um atributo dos próprios objetos. A lista se parece com:

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

Cada objeto tem uma contagem:

>>> ut[1].count
1L

Preciso classificar a lista pelo número de contagens decrescente.

Eu já vi vários métodos para isso, mas estou procurando as melhores práticas em Python.

Nick Sergeant
fonte
1
Classificação COMO FAZER para quem está procurando mais informações sobre classificação no Python.
Jeyekomon
1
além de operator.attrgetter ('attribute_name'), você também pode usar functors como chave como object_list.sort (key = my_sorting_functor ('my_key')), deixando a implementação intencionalmente.
Vijay Shanker

Respostas:

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)

Mais sobre a classificação por chaves .

Tríptico
fonte
1
Sem problemas. Aliás, se o muhuk estiver certo e for uma lista de objetos do Django, você deve considerar a solução dele. No entanto, para o caso geral de classificar objetos, minha solução é provavelmente a melhor prática.
Triptych
43
Em listas grandes, você obterá melhor desempenho usando operator.attrgetter ('count') como sua chave. Esta é apenas uma forma otimizada (nível inferior) da função lambda nesta resposta.
David Eyk
4
Obrigado pela ótima resposta. Caso seja uma lista de dicionários e 'count' seja uma de suas chaves, ele precisará ser alterado da seguinte forma: ut.sort (key = lambda x: x ['count'], reverse = True)
dganesh2002
Suponho que ele mereça a seguinte atualização: se houver a necessidade de classificar por vários campos, isso poderá ser alcançado por chamadas consecutivas para sort (), porque o python está usando o algoritmo de classificação estável.
zzz777 23/02
86

Uma maneira que pode ser mais rápida, principalmente se a sua lista tiver muitos registros, é usar operator.attrgetter("count"). No entanto, isso pode ser executado em uma versão pré-operador do Python, portanto, seria bom ter um mecanismo de fallback. Você pode fazer o seguinte, então:

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
fonte
7
Aqui eu usaria o nome da variável "keyfun" em vez de "cmpfun" para evitar confusão. O método sort () também aceita uma função de comparação através do argumento cmp =.
Akaihola
Isso não parece funcionar se o objeto tiver adicionado atributos dinamicamente (se você tiver feito self.__dict__ = {'some':'dict'}após o __init__método). Eu não sei por que deveria ser diferente, no entanto.
Tutuca
@tutuca: Eu nunca substituí a instância __dict__. Observe que "um objeto com atributos adicionados dinamicamente" e "definir o __dict__atributo de um objeto " são conceitos quase ortogonais. Estou dizendo isso porque seu comentário parece sugerir que definir o __dict__atributo é um requisito para adicionar atributos dinamicamente.
tzot
@tzot: Estou olhando para o seguinte: github.com/stochastic-technologies/goatfish/blob/master/… e usando o iterador aqui: github.com/TallerTechnologies/dishey/blob/master/app.py#L28 gera erro de atributo. Talvez por causa de python3, mas ainda assim ...
tutuca
1
@ tzot: se eu entender o uso de operator.attrgetter, eu poderia fornecer uma função com qualquer nome de propriedade e retornar uma coleção classificada.
IAbstract
64

Os leitores devem observar que o método key =:

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

é muitas vezes mais rápido do que adicionar operadores de comparação avançados aos objetos. Fiquei surpreso ao ler isso (página 485 de "Python em poucas palavras"). Você pode confirmar isso executando testes neste pequeno 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

Meus testes muito mínimos mostram que o primeiro tipo é 10 vezes mais lento, mas o livro diz que é apenas 5 vezes mais lento em geral. O motivo é que eles dizem que se deve ao algoritmo de classificação altamente otimizado usado em python ( timsort ).

Ainda assim, é muito estranho que .sort (lambda) seja mais rápido que o antigo .sort (). Espero que eles consertem isso.

Jose M Vidal
fonte
1
Definir __cmp__é equivalente a chamar .sort(cmp=lambda), não .sort(key=lambda), por isso não é nada estranho.
tzot 9/09/19
@tzot está exatamente certo. O primeiro tipo tem que comparar objetos um contra o outro várias vezes. A segunda classificação acessa cada objeto apenas uma vez para extrair seu valor de contagem e, em seguida, executa uma classificação numérica simples e altamente otimizada. Uma comparação mais justa seria longList2.sort(cmp = cmp). Eu tentei isso e ele executou quase o mesmo que .sort(). (Além disso: observe que o parâmetro de classificação "cmp" foi removido no Python 3.)
Bryan Roach
43

Abordagem orientada a objetos

É uma boa prática criar lógica de classificação de objetos, se aplicável, uma propriedade da classe em vez de incorporar em cada instância em que a ordem é necessária.

Isso garante consistência e elimina a necessidade de código padrão.

No mínimo, você deve especificar __eq__e __lt__operações para que isso funcione. Então é só usar 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
fonte
1
Era isso que eu estava procurando! Você poderia nos indicar alguma documentação que explique por que __eq__e quais __lt__são os requisitos mínimos de implementação?
FriendFX 7/08/19
1
@FriendFX, eu acredito que está implícito por este :•The sort routines are guaranteed to use __lt__() when making comparisons between two objects...
jpp
2
@FriendFX: Veja portingguide.readthedocs.io/en/latest/comparisons.html para comparação e classificação
Cornel Masson
37
from operator import attrgetter
ut.sort(key = attrgetter('count'), reverse = True)

fonte
16

Parece muito com uma lista de instâncias do modelo Django ORM.

Por que não classificá-los em consultas como esta:

ut = Tag.objects.order_by('-count')
muhuk
fonte
É, mas usando django-tagging, então eu estava usando um built-in para pegar um Tag definido pelo uso de um determinado conjunto de consultas, assim: Tag.objects.usage_for_queryset (QuerySet, counts = True)
Nick Sergeant
11

Adicione operadores de comparação avançados à classe de objeto e use o método sort () da lista.
Veja uma comparação rica em python .


Atualização : Embora esse método funcione, acho que a solução da Triptych é mais adequada ao seu caso, porque é muito mais simples.

roubar
fonte
3

Se o atributo que você deseja classificar for uma propriedade , evite importar operator.attrgettere usar os atributos da propriedade.fget método .

Por exemplo, para uma classe Circlecom uma propriedade, radiuspodemos classificar uma lista circlespor raio da seguinte maneira:

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

Esse não é o recurso mais conhecido, mas geralmente me poupa uma linha com a importação.

Georgy
fonte