如何在Python中按多个键对对象排序?


96

或者,实际上,如何按多个键对字典列表进行排序?

我有一个字典列表:

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):

对于传递给multikeysort函数的任意数量的键,如何使用lambda行对列表进行排序,并考虑到sortkey可以具有任意数量的键,并且需要识别需要反向排序的键前面加上“-”?

Answers:


72

此答案适用于字典中的任何列-否定的列不必是数字。

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

从这个答案中汲取灵感,从满足条件的可迭代项中获取第一项的最佳方法是什么?,我缩短了代码:

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(消除了的cmp参数sort):

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

受此答案启发,如何在Python 3中自定义排序?


这是最好的方法,因为我可以在任何键或列上使用反向键。谢谢!
simi

因此,这确实很好。我使用列表和字符串作为参数来调用函数。我首先拆分字符串,然后使用拆分后的字符串列表和键列表调用multikeysort。字符串中的哪个项目在列名的开头都带有'-'并不重要,因为它将与任何项目或所有项目一起使用。太棒了 谢谢。
simi

2
谢谢,您保存了我的一天!
Sander van Leeuwen 2014年

4
cmp()不适用于Python3,因此我不得不自己定义它,如下所述:stackoverflow.com/a/22490617/398514
pferate

8
@hughdbrown:您删除了cmp关键字,但是该cmp()函数仍在上面4行使用。我用3.2、3.3、3.4和3.5进行了尝试,它们都在函数调用中失败,因为cmp()未定义。此处的第三个项目符号(docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons)提到了cmp()视为已消失。
大约

53

本文对执行此操作的各种技术进行了简要介绍。如果您的需求比“完整的双向多键”简单,请看一下。很明显,我接受的答案和我刚刚引用的博客文章以某种方式相互影响,尽管我不知道顺序。

如果链接消失,这里是上面未涵盖的示例的简短摘要:

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']))

据我所知,stygianvision使用我的代码并且不给任何好评。Google forresult = cmp(fn(left), fn(right))
hughdbrown 2014年

4
感谢您的提要,Link实际上已经死了。:)
Amyth

47

我知道这是一个相当老的问题,但是没有答案提到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算法,因此实际上在实践中相当快。


2
非常好。对于中等数据集,这些数据集的排序无关紧要,这太酷了!如您所指出的,与sql排序相比,您必须反转python排序。谢谢。
格雷格,

第二种将破坏第一种的结果。有趣的是,没有支持者注意到它。
火山

9
有趣的是,您没有注意到主要排序标准排在最后,如我的示例所示,并在其他注释中进行了明确提及,以防万一您没有注意到它。
wouter bolsterlee

24
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']))

哇!太棒了。效果很好。我是一个新手,以至于我永远都无法了解所有这些。那也很快。非常感谢你。
simi

但是,如果发送到sortkeypicker的键是字符串,例如'-Total_Points,TOT_PTS_Misc',该怎么办?
simi

1
然后,您可以通过调用some_string.split(",")
Jason Creighton'Jul

谢谢。我已经评论了,我意识到我可以分割字符串。DOH!
simi

2
但是,如果您否定字符串值而不是数字值怎么办?我认为那不行。
尼克·帕金斯

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]]

2

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

这是我的解决方案的一种变体-适用于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}]

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之前进行拆分,这样,键列表中的每个项目都会检查第一个字符。我在正确的轨道上吗?
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)

这是行不通的。我有:值= [“-Total_Points”,“TOT_PTS_Misc”]则b为http://stardict.sourceforge.net/Dictionaries.php下载的列表中当我打电话克= itemgetter(值)(b)中,我得到AttributeError的:“列表”对象没有属性“startswith”
西米

它采用可变数量的名称,而不是名称列表。这样称呼它:itemgetter(* values)。再看一个类似的内置operator.itemgetter。
A. Coady
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.