In Python, I have something like the following (although randomly shuffled):
l = [('a', 'x'),
('a', 'y'),
('a', 'z'),
('b', 'x'),
('b', 'y'),
('b', 'z'),
]
If I call sorted(l)
, I get a sorted result (like above) which is what one would expect. However, what I need is to forward sort on the first element of the tuple and reverse sort on the second element. In other words, I would like the following result:
l = [('a', 'z'),
('a', 'y'),
('a', 'x'),
('b', 'z'),
('b', 'y'),
('b', 'x'),
]
In Python2.x, there exists a cmp
parameter that can be passed to sorted()
to achieve this result, but Python3 no longer has this. It has only a key
parameter. Is there any way to achieve the desired sort order using only the key
parameter?
I know I can define a whole new class to wrap my tuples, or use something like functools.cmp_to_key
(which also creates a wrapper class), but that all seems heavy handed for such a simple operation. Is there no other recourse?
edit: I should add that the strings won't all be one-character strings and that, in some cases, the list of tuples contain non-string data [i.e. (basestring, datetime)].
In a single step:
>>> l.sort(key=lambda t: (t[0], -ord(t[1])))
>>> l
[('a', 'z'), ('a', 'y'), ('a', 'x'), ('b', 'z'), ('b', 'y'), ('b', 'x')]
Any time you need to sort on multiple keys you can make your key function a tuple since tuples are compared lexicographically. If you need to reverse sort on one of the keys just make that element negative. Obviously you can't just make the string negative so you first need to convert it to an integer with ord
.
The Python Sorting HOWTO recommends that you take advantage of sort stability and do the sort in two passes:
>>> l.sort(key=lambda t: t[1], reverse=True) # SECONDARY KEY: field 1 descending
>>> l.sort(key=lambda t: t[0]) # PRIMARY KEY: field 0 ascending
>>> l
[('a', 'z'), ('a', 'y'), ('a', 'x'), ('b', 'z'), ('b', 'y'), ('b', 'x')]
Sorting in Python is stable as of Python 2.2.
So, you can sort by the second value first with the reverse flag on:
>>> from operator import itemgetter
>>> l.sort(key=itemgetter(1), reverse=True)
>>> l
[('a', 'z'), ('b', 'z'), ('a', 'y'), ('b', 'y'), ('a', 'x'), ('b', 'x')]
Then you can sort by the first value:
>>> l.sort(key=itemgetter(0))
>>> l
[('a', 'z'), ('a', 'y'), ('a', 'x'), ('b', 'z'), ('b', 'y'), ('b', 'x')]
You could accomplish this by running sorted twice on the list, first using a reverse sort with the second item in each tuple, then using a normal sort on the newly sorted list with the first item in each tuple.
l = [('a', 'y'),
('a', 'x'),
('b', 'y'),
('b', 'x'),
('b', 'z'),
('a', 'z'),
]
l = sorted(l, key=lambda tup: tup[1], reverse=True)
l = sorted(l, key=lambda tup: tup[0])
Or, if you prefer:
l = sorted(sorted(l, key=lambda tup: tup[1], reverse=True),
key=lambda tup: tup[0])
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