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

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

我有一系列的措辞:

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 函数的任意数量的键进行排序,并考虑到 sortkey 可能有任意数量的键,而那些需要反向排序的键将在其前面标识为“-”?

137115 次浏览
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']))
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'}]

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

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

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(它消除了 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))

受这个答案的启发

既然您已经对 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)

我使用以下内容对一个2d 数组的多个列进行排序

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

本文 对实现这一点的各种技术进行了很好的总结。如果您的需求比“全双向多键”更简单,请看一下。很明显,接受的答案和博客文章我只是 相互影响,虽然我不知道是哪个顺序。

如果链接中断了,这里有一个上面没有提到的例子的快速概要:

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

我知道这是一个相当古老的问题,但是没有一个答案提到 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'))

请注意项目是如何首先按照“ less”属性 age(降序)排序,然后按照“ main”属性 name排序的,从而得到正确的最终顺序。

反转/反转适用于所有可排序的类型,而不仅仅是那些你可以通过在前面放一个减号来否定的数字。

由于(至少) CPython 中使用了 Timsort 算法,因此在实际中这实际上相当快。

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

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