【问题标题】:How to sort objects by multiple keys?如何按多个键对对象进行排序?
【发布时间】:2010-11-11 17:38:50
【问题描述】:

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

我有一个字典列表:

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 行对列表进行排序,并考虑到排序键可能具有任意数量的键以及需要反向排序的键会在它之前用'-'标识吗?

【问题讨论】:

标签: python sorting dictionary


【解决方案1】:

This article 对各种技术进行了很好的总结。如果您的要求比“完全双向多键”更简单,请查看。很明显接受的答案和我刚刚发布的博客文章 引用在某种程度上相互影响,虽然我不知道是哪个顺序。

如果链接失效,这里是上面未涵盖的示例的快速概要:

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 使用我的代码并且不给予任何信任。谷歌result = cmp(fn(left), fn(right))
  • 感谢您的概要,Link 现在实际上已经死了。 :)
  • 谢谢,正是我想要的。这应该是公认的答案。
  • 提供的链接中没有详细的解释,只是一个已经很明显的一般想法。这里是python官方网站上更好的解释:docs.python.org/3/howto/sorting.html
【解决方案2】:

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

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-01-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。字符串中的哪个项目在列名的开头有“-”并不重要,因为它适用于任何一个项目或所有项目。惊人的。谢谢。
  • 假设 dicts (b) 列表中的项目没有我想要排序的键。我将如何测试他们?我尝试了 try/except,但它没有返回任何内容。
  • cmp() 不适用于 Python3,所以我必须自己定义它,如此处所述:stackoverflow.com/a/22490617/398514
  • @hughdbrown:您删除了 cmp 关键字,但上面 4 行仍然使用了 cmp() 函数。我用 3.2、3.3、3.4 和 3.5 尝试过,它们都在函数调用中失败,因为未定义 cmp()。这里的第三个项目符号 (docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons) 提到将 cmp() 视为已消失。
【解决方案3】:

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

【讨论】:

  • 非常好。对于中等数据集,多次排序并不重要,这太酷了!正如您所指出的,与 sql 排序相比,您必须反转 python 排序。谢谢。
  • 第二次排序会破坏第一次的结果。有趣的是,没有一个支持者注意到它。
  • 有趣的是,您没有注意到主要排序标准排在最后,如我的示例所示,并在另一条评论中明确提到,以防万一您没有注意到。
  • 似乎在主要排序标准上使用reverse=True 搞砸了?尝试使用列表l = [[4, "b"], [1, "a"], [2, "a"], [2, "b"], [3, "b"]]
  • 似乎还不错:&gt;&gt;&gt; l = [[4, "b"], [1, "a"], [2, "a"], [2, "b"], [3, "b"]] &gt;&gt;&gt; random.shuffle(l) &gt;&gt;&gt; l [[4, 'b'], [1, 'a'], [2, 'b'], [3, 'b'], [2, 'a']] &gt;&gt;&gt; l.sort(key=itemgetter(1)) &gt;&gt;&gt; l.sort(key=itemgetter(0), reverse=True) &gt;&gt;&gt; l [[4, 'b'], [3, 'b'], [2, 'a'], [2, 'b'], [1, 'a']]
【解决方案4】:
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'?
  • 然后你可以先调用some_string.split(",")将字符串拆分成一个数组
  • 但是如果你否定字符串值而不是数字值呢?我认为这行不通。
【解决方案5】:

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

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

【讨论】:

    【解决方案6】:

    我使用以下方法对多列上的二维数组进行排序

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

    【讨论】:

      【解决方案7】:
      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'}]
      

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

      【讨论】:

      • 但是,当字符串中的第二项带有“-”时,它会给我一个错误的一元操作数类型 - 错误。
      • 字符串不能取负数。
      • 是的,我知道,但这是传入参数的方式。即使我进行拆分,一个或另一个将以'-'开头。我认为在调用 key_getter 之前需要拆分排序键,这样键列表中的每个项目都会检查第一个字符。我在正确的轨道上吗?
      【解决方案8】:

      既然您已经习惯了 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'跨度>
      • 它采用可变数量的名称,而不是名称列表。像这样调用它:itemgetter(*values)。看看类似的内置 operator.itemgetter 的另一个例子。
      猜你喜欢
      • 1970-01-01
      • 2023-02-03
      • 1970-01-01
      • 2021-11-05
      • 1970-01-01
      • 2020-12-04
      • 2012-12-10
      • 2020-07-11
      • 1970-01-01
      相关资源
      最近更新 更多