如何基于对象的属性对列表进行排序?

1212

我有一个Python对象列表,希望按照每个对象的特定属性进行排序:

[Tag(name="toe", count=10), Tag(name="leg", count=2), ...]

如何按降序排列列表中的.count


2
重复问题:https://dev59.com/kXVC5IYBdhLWcg3w4VRz,https://dev59.com/k3VC5IYBdhLWcg3wpi98,https://dev59.com/a0XRa4cB1Zd3GeqPveNt - S.Lott
6
《Python排序指南》是为那些想要了解Python中排序更多信息的人准备的。请参阅:https://docs.python.org/3/howto/sorting.html - Jeyekomon
1
除了使用 operator.attrgetter('attribute_name') 之外,您还可以像 object_list.sort(key=my_sorting_functor('my_key')) 这样使用函数对象作为键,故意省略实现细节。 - vijay shanker
9个回答

1918
在原列表上进行排序:

要在原列表上进行排序:

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

要返回一个新的列表,请使用 sorted

new_list = sorted(orig_list, key=lambda x: x.count, reverse=True)

说明:

  • key=lambda x: x.count 按计数排序。
  • reverse=True 按降序排列。

更多关于按键排序的信息。


2
没问题。顺便说一句,如果muhuk是正确的,那么它是一个Django对象列表,你应该考虑他的解决方案。然而,对于排序对象的一般情况,我的解决方案可能是最佳实践。 - Kenan Banks
66
在大型列表上,使用operator.attrgetter('count')作为键值可以获得更好的性能。这只是此答案中lambda函数的优化(底层)形式。 - David Eyk
8
感谢您提供这个好答案。如果数据是一个由字典组成的列表,其中'count'是一个键,则需要进行以下更改:ut.sort(key=lambda x: x['count'], reverse=True) - dganesh2002
2
我认为这个更新是必要的:如果需要按多个字段排序,可以通过连续调用sort()来实现,因为Python使用的是稳定的排序算法。 - uuu777
1
谢谢@KenanBanks,你说的对。令人恼火的是,Outlook在处理日历时区方面存在一些奇怪的问题,以至于有些日历没有时区细节……不知道为什么! - peetysmith
显示剩余3条评论

115

如果你的列表有很多记录,特别是在使用operator.attrgetter("count")时,它是最快的方法。但是,这可能会在Python的早期版本中运行,因此最好具备后备机制。那么你可以考虑以下做法:

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

8
为避免混淆,我会使用变量名“keyfun”代替“cmpfun”。sort()方法也可以通过cmp=参数接受比较函数。 - akaihola
如果对象具有动态添加的属性(如果在__init__方法之后执行了self.__dict__ = {'some':'dict'}),则此方法似乎无法正常工作。虽然我不知道为什么会有所不同。 - tutuca
@tzot,这与django无关,goatfish元属性只是一个没有任何魔法的原始对象...我在Python 2.7项目中对其进行了测试,似乎按预期工作。我需要进一步阅读该问题... - tutuca
1
如果我理解operator.attrgetter的用法,我可以提供一个带有任何属性名称的函数并返回一个排序后的集合。 - IAbstract
1
寻找更多信息的人可以访问以下链接:https://wiki.python.org/moin/HowTo/Sorting#Operator_Module_Functions - alekosot
显示剩余4条评论

93

读者应注意key=方法:

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

据《Python in a Nutshell》第485页所述,使用运算符模块比为对象添加丰富的比较运算符快很多。通过运行以下小程序可以证实这一点:

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

我的极简测试结果显示,第一种排序比第二种慢超过10倍,但书中说通常只慢约5倍。他们说的原因是Python使用了高度优化的排序算法timsort

尽管如此,.sort(lambda)比普通的.sort()更快还是很奇怪。我希望他们能解决这个问题。


4
定义__cmp__等同于调用.sort(cmp=lambda)而不是.sort(key=lambda),所以这一点并不奇怪。 - tzot
@tzot 是完全正确的。第一种排序方法需要反复比较对象。而第二种排序方法只需要访问每个对象一次,提取其计数值,然后执行高度优化的简单数字排序。更公平的比较应该是 longList2.sort(cmp = cmp)。我尝试了一下,它的表现几乎和 .sort() 一样。(另外注意,在Python 3中,“cmp”排序参数已被移除。) - Bryan Roach
4
__cmp__在Python 3中已被弃用:https://docs.python.org/3/howto/sorting.html#the-old-way-using-the-cmp-parameter - neves

80

面向对象方法

如果适用,将对象排序逻辑作为类的属性而不是合并到需要排序的每个实例中,是一个好的实践。

这样确保了一致性,并消除了样板代码的需求。

至少,您应该为此指定__eq____lt__操作才能使其正常工作。然后只需使用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]

3
这正是我正在寻找的!你可以指向一些详细解释为什么__eq____lt__是最小实现要求的文档吗? - FriendFX
4
@FriendFX,我相信这是通过此链接中的内容推论出来的:“•排序例程在比较两个对象时保证使用__lt__()方法…” - jpp
2
@FriendFX:请查看https://portingguide.readthedocs.io/en/latest/comparisons.html以获取比较和排序信息。 - Cornel Masson
有没有办法自动将所有特殊的二进制比较方法转发到类的一个属性上,而不是实现__eq____lt____le____gt____ge____ne__,然后在内部转发到属性的特殊函数? - j-hap
我刚刚编写了自己的装饰器来完成我在之前评论中想要实现的功能。这真的很丑陋,最好还是实现__eq____lt__,然后使用@functools.total_ordering来实现其余部分。 - j-hap

46
from operator import attrgetter
ut.sort(key = attrgetter('count'), reverse = True)

17

它看起来非常像Django ORM模型实例的列表。

为什么不像这样在查询中对它们进行排序:

ut = Tag.objects.order_by('-count')

是的,但是使用django-tagging,因此我正在使用内置功能来捕获特定查询集的标签集,如下所示:Tag.objects.usage_for_queryset(QuerySet, counts=True) - Nick Sergeant

15
如果您想按属性排序,则可以避免导入operator.attrgetter并使用该属性的fget方法。例如,对于具有属性半径的类Circle,我们可以按以下方式按半径对圆列表进行排序:
result = sorted(circles, key=Circle.radius.fget)

这不是最为人所知的功能,但通常可以通过导入少一行代码。


我很惊讶我不能只写Circle.radius。我不明白为什么它不起作用。谢谢你指出使用fget。如果其他方法可以完成任务,我不喜欢定义lambda函数,也不喜欢将属性名称作为字符串传递给attrgetter,对我来说感觉不对劲。 - Peter Wood
1
@PeterWood 关键是需要 (1) 可调用,(2) 接受一个参数,并且 (3) 返回一个可排序的值。sorted 函数会将每个要排序的项依次传递给关键字函数,并将返回的值作为代理来排序这些项。Circle.radius 不起作用,因为 property 对象本身不可调用。但是它们有一个名为 fget 的属性是可调用的。 - undefined
@ibonyun 谢谢,这真的帮助澄清了为什么。 - undefined

13
给对象类添加丰富的比较运算符,然后使用列表的sort()方法。
请参阅Python中的丰富比较

更新: 尽管这种方法可以实现,但我认为Triptych提供的解决方案更适合您的情况,因为更简单。


4

如果有人想要对包含字符串和数字的列表进行排序,可以这样做:

 eglist=[
     "some0thing3",
     "some0thing2",
     "some1thing2",
     "some1thing0",
     "some3thing10",
     "some3thing2",
     "some1thing1",
     "some0thing1"]

那么这是实现它的代码:
import re

def atoi(text):
    return int(text) if text.isdigit() else text

def natural_keys(text):
    return [ atoi(c) for c in re.split(r'(\d+)', text) ]

eglist=[
         "some0thing3",
         "some0thing2",
         "some1thing2",
         "some1thing0",
         "some3thing10",
         "some3thing2",
         "some1thing1",
         "some0thing1"
]

eglist.sort(key=natural_keys)
print(eglist)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接