如何按多个键对对象进行排序?

142

或者说,我如何在实践中按多个键对包含字典的列表进行排序?

我有一个字典列表:

b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

我需要使用按Total_Points反向排序的多键排序,然后按TOT_PTS_Misc不反向排序。

可以在命令提示符中这样完成:

a = sorted(b, key=lambda d: (-d['Total_Points'], d['TOT_PTS_Misc']))

但是我必须通过一个函数来运行它,该函数需要传入列表和排序键。例如,def multikeysort(dict_list, sortkeys):

如何使用lambda行对列表进行排序,对于传递给multikeysort函数的任意数量的键,并考虑到sortkeys可能具有任意数量的键以及那些需要进行反向排序的键将在其前面标有'-'?


8个回答

120
这篇文章详细介绍了实现多键排序的不同技术方法。如果你只需要比“完全双向多键”更简单的需求,可以看一下。很明显,被接受的答案和我刚提到的博客文章在某种程度上互相影响,但我不知道它们的顺序。

如果链接失效了,这里有一些未覆盖的示例:

mylist = sorted(mylist, key=itemgetter('name', 'age'))
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), k['age']))
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), -k['age']))

1
据我所知,stygianvision使用了我的代码但并未给予任何功劳。在Google中搜索 result = cmp(fn(left), fn(right)) - hughdbrown
5
谢谢提供概要,Link现在已经去世了。 :) - Amyth
谢谢,这正是我在寻找的。也许这应该成为被接受的答案。 - philx_x
2
提供的链接中没有详细的解释,只有一些显而易见的概念。这里是官方 Python 网站上更好的解释:https://docs.python.org/3/howto/sorting.html - user3151858

94

这个答案适用于字典中的任何类型的列--否定的列不一定是一个数字。

def multikeysort(items, columns):
    from operator import itemgetter
    comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else
                  (itemgetter(col.strip()), 1)) for col in columns]
    def comparer(left, right):
        for fn, mult in comparers:
            result = cmp(fn(left), fn(right))
            if result:
                return mult * result
        else:
            return 0
    return sorted(items, cmp=comparer)
你可以这样调用它:
b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

a = multikeysort(b, ['-Total_Points', 'TOT_PTS_Misc'])
for item in a:
    print item

尝试使用任意一列的负数进行测试,您将会看到排序顺序发生反转。

接下来:将其更改为不使用额外类...


2016-01-17

受这个答案的启发What is the best way to get the first item from an iterable matching a condition?,我缩短了代码:

from operator import itemgetter as i

def multikeysort(items, columns):
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, cmp=comparer)

如果您喜欢简洁的代码。


稍晚于2016年1月17日

这适用于Python3(其中取消了sortcmp参数):

from operator import itemgetter as i
from functools import cmp_to_key

def cmp(x, y):
    """
    Replacement for built-in function cmp that was removed in Python 3

    Compare the two objects x and y and return an integer according to
    the outcome. The return value is negative if x < y, zero if x == y
    and strictly positive if x > y.

    https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function
    """

    return (x > y) - (x < y)

def multikeysort(items, columns):
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, key=cmp_to_key(comparer))

受这个答案的启发 How should I do custom sort in Python 3?


所以这个很好用。我使用列表和字符串作为参数调用我的函数。我首先拆分字符串,然后使用列表和来自拆分字符串的键列表调用multikeysort。无论哪个项目在字符串中以'-'开头的列名,都没有关系,因为它可以与任何项目或所有项目一起使用。太棒了。谢谢。 - simi
这不会对第二列进行排序,只会对第一列进行排序。 - Joel
4
Python3 中没有 cmp() 函数,所以我不得不自己定义它,就像这里提到的一样:https://dev59.com/9WEh5IYBdhLWcg3wYSc2#22490617。 - pferate
@pferate:标记为“Later 2016-01-17 This works with python3 (which eliminated the cmp argument to sort)”的代码对您无效吗? - hughdbrown
8
您删除了cmp关键字,但在上面的4行中仍然使用了cmp()函数。我尝试在3.2、3.3、3.4和3.5上使用它们,它们在函数调用时都失败了,因为cmp()未定义。这里的第三个要点(https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons)提到将`cmp()`视为已删除。 - pferate
显示剩余13条评论

73

我知道这是一个比较老的问题,但是所有回答都没有提到Python对其排序程序(如list.sort()sorted())保证稳定排序顺序,这意味着比较相等的项会保留它们的原始顺序。

这意味着可以像这样对字典列表执行类似于ORDER BY name ASC, age DESC(使用SQL表示法)的操作:

items.sort(key=operator.itemgetter('age'), reverse=True)
items.sort(key=operator.itemgetter('name'))

注意首先按照“较小”的属性age (降序)排序,然后按照“主要”的属性name排序,得到正确的最终顺序。

反转/倒置适用于所有可排序类型,不仅仅是可以通过在前面加负号来进行取反的数字。

并且由于至少在 CPython 中使用了 Timsort 算法,所以实际上这是相当快的。


3
非常好。对于中等规模的数据集,重复排序不是问题,这太棒了!正如你所指出的,与SQL排序相比,必须反转Python排序。谢谢。 - Greg
1
第二个排序将打破第一个的结果。有趣的是,没有一个点赞者注意到这一点。 - volcano
17
有趣的是你没有注意到主要排序标准放在最后,就像我示例中展示的那样,并在其他评论中明确提到以便让你清楚地了解。 - wouter bolsterlee
似乎在主排序标准上使用 reverse=True 会搞砸这个?请尝试使用列表 l = [[4,"b"],[1,"a"],[2,"a"],[2,"b"],[3,"b"]] - Locane
看起来很好:`>>> l = [[4, "b"], [1, "a"], [2, "a"], [2, "b"], [3, "b"]]
random.shuffle(l) l [[4, 'b'], [1, 'a'], [2, 'b'], [3, 'b'], [2, 'a']] l.sort(key=itemgetter(1)) l.sort(key=itemgetter(0), reverse=True) l [[4, 'b'], [3, 'b'], [2, 'a'], [2, 'b'], [1, 'a']]`
- wouter bolsterlee

25
def sortkeypicker(keynames):
    negate = set()
    for i, k in enumerate(keynames):
        if k[:1] == '-':
            keynames[i] = k[1:]
            negate.add(k[1:])
    def getit(adict):
       composite = [adict[k] for k in keynames]
       for i, (k, v) in enumerate(zip(keynames, composite)):
           if k in negate:
               composite[i] = -v
       return composite
    return getit

a = sorted(b, key=sortkeypicker(['-Total_Points', 'TOT_PTS_Misc']))

但是,如果发送到sortkeypicker的键是字符串,例如'-Total_Points,TOT_PTS_Misc',该怎么办? - simi
1
然后,您可以通过调用 some_string.split(",") 将字符串首先拆分为数组。 - Jason C
2
但是如果你否定字符串值而不是数字值呢?我认为那样行不通。 - Nick Perkins

15

今天我遇到了类似的问题 - 我需要按照数值降序和字符串升序对字典项进行排序。为了解决方向冲突的问题,我取反了整数值。

这是我的解决方案的一个变体 - 适用于 OP。

sorted(b, key=lambda e: (-e['Total_Points'], e['TOT_PTS_Misc']))

非常简单 - 并且像魔法一样奏效

[{'TOT_PTS_Misc': 'Chappell, Justin', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Russo, Brandon', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Utley, Alex', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Foster, Toney', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Lawson, Roman', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Lempke, Sam', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Gnezda, Alex', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Kirks, Damien', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Korecz, Mike', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Worden, Tom', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Burgess, Randy', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Harmon, Gary', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Smugala, Ryan', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Swartz, Brian', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Blackwell, Devon', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Blasinsky, Scott', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Bolden, Antonio', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Carter III, Laymon', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Coleman, Johnathan', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Kovach, Alex', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Smith, Ryan', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Venditti, Nick', 'Total_Points': 60.0}]

5

我使用以下方法对一个二维数组按照多列进行排序:

def k(a,b):
    def _k(item):
        return (item[a],item[b])
    return _k

这可以扩展到任意数量的项目。我倾向于认为找到更好的可排序键访问模式比编写复杂的比较器更好。
>>> data = [[0,1,2,3,4],[0,2,3,4,5],[1,0,2,3,4]]
>>> sorted(data, key=k(0,1))
[[0, 1, 2, 3, 4], [0, 2, 3, 4, 5], [1, 0, 2, 3, 4]]
>>> sorted(data, key=k(1,0))
[[1, 0, 2, 3, 4], [0, 1, 2, 3, 4], [0, 2, 3, 4, 5]]
>>> sorted(a, key=k(2,0))
[[0, 1, 2, 3, 4], [1, 0, 2, 3, 4], [0, 2, 3, 4, 5]]

0
from operator import itemgetter
from functools import partial

def _neg_itemgetter(key, d):
    return -d[key]

def key_getter(key_expr):
    keys = key_expr.split(",")
    getters = []
    for k in keys:
        k = k.strip()
        if k.startswith("-"):
           getters.append(partial(_neg_itemgetter, k[1:]))
        else:
           getters.append(itemgetter(k))

    def keyfunc(dct):
        return [kg(dct) for kg in getters]

    return keyfunc

def multikeysort(dict_list, sortkeys):
    return sorted(dict_list, key = key_getter(sortkeys)

演示:

>>> multikeysort([{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 60.0},
                 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0}, 
                 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0}],
                "-Total_Points,TOT_PTS_Misc")
[{u'Total_Points': 96.0, u'TOT_PTS_Misc': u'Chappell, Justin'}, 
 {u'Total_Points': 96.0, u'TOT_PTS_Misc': u'Russo, Brandon'}, 
 {u'Total_Points': 60.0, u'TOT_PTS_Misc': u'Utley, Alex'}]

解析有点脆弱,但至少允许在键之间使用可变数量的空格。


但是,当字符串中有第二个项目为'-'时,会提示一元减法运算符类型错误。 - simi
你不能对一个字符串取负。 - Torsten Marek
是的,我知道,但这就是参数传递的方式。即使我进行拆分,其中一个也将以“-”开头。我认为在调用key_getter之前需要拆分sortkeys,这样键列表中的每个项目都将检查第一个字符。我走在正确的轨道上吗? - simi

0

既然您已经熟悉了lambda,这里提供一种更简洁的解决方案。

>>> def itemgetter(*names):
    return lambda mapping: tuple(-mapping[name[1:]] if name.startswith('-') else mapping[name] for name in names)

>>> itemgetter('a', '-b')({'a': 1, 'b': 2})
(1, -2)

这个不起作用。我有: values = ['-Total_Points', 'TOT_PTS_Misc'] 然后b是字典列表 当我调用g = itemgetter(values)(b)时,我得到AttributeError:'list' object has no attribute 'startswith' - simi
它接受可变数量的名称,而不是名称列表。像这样调用它:itemgetter(* values)。查看类似的内置operator.itemgetter以获取另一个示例。 - A. Coady

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