Reputation: 2993
In Sorting a Python list by two criteria Fouad gave the following answer:
sorted(list, key=lambda x: (x[0], -x[1]))
I'd like to sort the following list primarily on the list of tuples primarily on the second item in each element in ascending order, followed by the first (alphabetic) item in descending order:
[('Ayoz', 1, 18, 7), ('Aidan', 2, 4, 9), ('Alan', 2, 4, 9), ('Arlan', 5, 6, 7), ('Luke', 15, 16, 2), ('Tariq', 5, 4, 2)]
to give the answer:
[('Ayoz', 1, 18, 7), ('Alan', 2, 4, 9), ('Aidan', 2, 4, 9), ('Tariq', 5, 4, 2), ('Arlan', 5, 6, 7), ('Luke', 15, 16, 2)]
using the above approach if possible . I tried
tlist = [('Ayoz', 1, 18, 7), ('Aidan', 2, 4, 9), ('Alan', 2, 4, 9), ('Arlan', 5, 6, 7), ('Luke', 15, 16, 2), ('Tariq', 5, 4, 2)]
sorted(tlist, key=lambda elem: (elem[1], -elem[0]))
but that only works when elem[0] is numeric (in this case it gives a TypeError: bad operand type for unary -: 'str')
I'll be grateful for any help. Python version is 3.4
Upvotes: 5
Views: 3364
Reputation: 57474
I just ran into this, and I was surprised to find that it's a missing piece in Python's otherwise robust sorting. You can reverse the overall sort, but you can't reverse subsorts. The other answers are usable workarounds, but they aren't general solutions if you have a list of fields you want to sort with varying subsort order, especially if multiple-pass sorting isn't an option.
This is what I ended up with:
class inverse_str(str):
"""
A string that sorts in inverse order.
"""
def __lt__(self, rhs):
return not super().__lt__(rhs)
data = [('Ayoz', 1, 18, 7), ('Aidan', 2, 4, 9), ('Alan', 2, 4, 9), ('Arlan', 5, 6, 7), ('Luke', 15, 16, 2), ('Tariq', 5, 4, 2)]
data.sort(key=lambda item: (item[1], inverse_str(item[0])))
print(data)
wanted = [('Ayoz', 1, 18, 7), ('Alan', 2, 4, 9), ('Aidan', 2, 4, 9), ('Tariq', 5, 4, 2), ('Arlan', 5, 6, 7), ('Luke', 15, 16, 2)]
print(data == wanted)
That has the same effect as returning eg. 1-item[1] from the key function for a float field.
RootTwo's multi-stage sort answer is valid, but multiple-step sorting isn't always an option. I'm merging incremental previously-sorted streams of data using heapq.merge, so that won't work. Doing it within the comparison does this within the actual ordering, so you don't need multiple passes and it works in the general case.
Upvotes: 1
Reputation: 4418
Here's an alternative. It works like the trick to use -number to reverse the order of a sort, but applies to strings of letters.
The table below maps 'a' to 'z', 'b' to 'y', etc. t[0].translate(table)
translates 'Ayoz' to 'Zbla', so the key for ('Ayoz', 1, 18, 7) is (1, Zbla')
table = str.maketrans('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
'zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA')
tlist.sort(key=lambda t: (t[1], t[0].translate(table)))
Upvotes: 0
Reputation: 4418
The built in sorting routines in Python are stable. That is, if two items have the same key value, then they keep the order they had relative to each other (the one closer to the front of the list stays closer to the front). So you can sort on multiple keys using multiple sorting passes.
from operator import itemgetter
tlist = [('Ayoz', 1, 18, 7), ('Aidan', 2, 4, 9), ('Alan', 2, 4, 9),
('Arlan', 5, 6, 7), ('Luke', 15, 16, 2), ('Tariq', 5, 4, 2)]
# sort by name in descending order
tlist.sort(key=itemgetter(0), reverse=True)
print('pass 1:', tlist)
# sort by element 1 in ascending order. If two items have the same value
# the names stay in the same order they had (descending order)
tlist.sort(key=itemgetter(1))
print(npass 2:', tlist)
Prints:
pass 1: [('Tariq', 5, 4, 2), ('Luke', 15, 16, 2), ('Ayoz', 1, 18, 7), ('Arlan', 5, 6, 7), ('Alan', 2, 4, 9), ('Aidan', 2, 4, 9)]
pass 2: [('Ayoz', 1, 18, 7), ('Alan', 2, 4, 9), ('Aidan', 2, 4, 9), ('Tariq', 5, 4, 2), ('Arlan', 5, 6, 7), ('Luke', 15, 16, 2)]
Upvotes: 3
Reputation: 594
You can give the original answer a twist to get it work:
sorted(tlist, key=lambda elem: (-elem[1], elem[0]), reverse=True)
Upvotes: 2
Reputation: 2993
tlist = [('Ayoz', 1, 18, 7), ('Alan', 2, 4, 9), ('Aidan', 2, 4, 9), ('Arlan', 5, 6, 7), ('Luke', 15, 16, 2), ('Tariq', 5, 4, 2)]
sorted(tlist, key=lambda elem: (elem[1],sorted(elem[0],reverse=True)))
Worked it out but it took me half an hour to type so I'm posting no matter what. I still welcome a better way of doing it.
Upvotes: 2