Or, practically, how can I sort a list of dictionaries by multiple keys?
I have a list of dicts:
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}]
and I need to use a multi key sort reversed by Total_Points, then not reversed by TOT_PTS_Misc
.
This can be done at the command prompt like so:
a = sorted(b, key=lambda d: (-d['Total_Points'], d['TOT_PTS_Misc']))
But I have to run this through a function, where I pass in the list and the sort keys. For example, def multikeysort(dict_list, sortkeys):
.
How can the lambda line be used which will sort the list, for an arbitrary number of keys that are passed in to the multikeysort function, and take into consideration that the sortkeys may have any number of keys and those that need reversed sorts will be identified with a '-' before it?
@moose, @Amyth, to reverse to only one attribute, you can sort twice: first by the secondary s = sorted(s, key = operator. itemgetter(2)) then by the primary s = sorted(s, key = operator.
This article has a nice rundown on various techniques for doing this. If your requirements are simpler than "full bidirectional multikey", take a look. It's clear the accepted answer and the blog post I just referenced influenced each other in some way, though I don't know which order.
In case the link dies here's a very quick synopsis of examples not covered above:
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']))
This answer works for any kind of column in the dictionary -- the negated column need not be a number.
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)
You can call it like this:
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
Try it with either column negated. You will see the sort order reverse.
Next: change it so it does not use extra class....
2016-01-17
Taking my inspiration from this answer What is the best way to get the first item from an iterable matching a condition?, I shortened the code:
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)
In case you like your code terse.
Later 2016-01-17
This works with python3 (which eliminated the cmp
argument to 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))
Inspired by this answer How should I do custom sort in Python 3?
I know this is a rather old question, but none of the answers mention that Python guarantees a stable sort order for its sorting routines such as list.sort()
and sorted()
, which means items that compare equal retain their original order.
This means that the equivalent of ORDER BY name ASC, age DESC
(using SQL notation) for a list of dictionaries can be done like this:
items.sort(key=operator.itemgetter('age'), reverse=True)
items.sort(key=operator.itemgetter('name'))
Note how the items are first sorted by the "lesser" attribute age
(descending), then by the "major" attribute name
, leading to the correct final order.
The reversing/inverting works for all orderable types, not just numbers which you can negate by putting a minus sign in front.
And because of the Timsort algorithm used in (at least) CPython, this is actually rather fast in practice.
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']))
I had a similar issue today - I had to sort dictionary items by descending numeric values and by ascending string values. To solve the issue of conflicting directions, I negated the integer values.
Here's a variant of my solution - as applicable to OP
sorted(b, key=lambda e: (-e['Total_Points'], e['TOT_PTS_Misc']))
Very simple - and works like a charm
[{'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}]
I use the following for sorting a 2d array on a number of columns
def k(a,b):
def _k(item):
return (item[a],item[b])
return _k
This could be extended to work on an arbitrary number of items. I tend to think finding a better access pattern to your sortable keys is better than writing a fancy comparator.
>>> 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]]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With