Underlying theory
All of the following applies to both the built-in sorted
function, and the .sort
method of lists.
In general, a key
function for sorting can simply produce a tuple, where each element corresponds to one of the "keys" we want to use for sorting. These tuples will sort lexicographically, so this produces the desired result - elements are sorted according to the first key result, with ties broken by the second, etc.
Meanwhile, the reverse
keyword argument for sorting can specify that sorting should be done in reverse order. It's equivalent to sorting normally, and then reversing the result, but more efficient.
However, this reverse
setting applies to the entire sort. It does not allow for sorting ascending by one key and then descending by a different key, or vice versa.
Example setup
It is possible to sort a list containing any sort of objects, not just nested lists/tuples; and it is possible to write key functions that process those objects in whatever manner - for example, to sort instances of a class according to the value of a specific attribute. For clarity (i.e., in order to use attribute names), I will set up a simple namedtuple
and demonstrate techniques to sort a list of instances.
from collections import namedtuple
datum = namedtuple('datum', 'id age first last')
data = [
datum(1, 23, 'Foo', 'Bar'),
datum(2, 42, 'Baz', 'Quux'),
# etc.
]
Special case: sorting by two numeric keys
To emulate sorting in reverse, it is sufficient to take the negative of a numeric value. Thus:
# sort ascending by id, then descending by age
data.sort(key=lambda d: (d.id, -d.age))
# equivalent, but more complex:
data.sort(key=lambda d: (-d.id, d.age), reverse=True)
Special case: sorting by at most one non-numeric key
If there is only one non-numeric key, choosing whether or not to use reverse
allows us to avoid the issue that only numeric keys can be negated in this way:
# sort ascending by first name, then descending by id
data.sort(key=lambda d: (d.first, -d.id))
# sort ascending by age, then descending by last name
# since the name can't be negated, `reverse` is needed;
# this implies in turn that the age values should be negated.
data.sort(key=lambda d: (-d.age, d.last), reverse=True)
Using a wrapper to negate values
A more general approach is to create a wrapper class negated
, with the semantics that negated(x) < negated(y)
if and only if x >= y
. This is the approach taken in black panda's answer. Thus:
class negated: # name changed; otherwise the same
def __init__(self, obj):
self.obj = obj
def __eq__(self, other):
return other.obj == self.obj
def __lt__(self, other):
return other.obj < self.obj
# Sort descending by last name, then ascending by first name.
data.sort(lambda d: (negated(d.last), d.first))
More sophisticated: adapting a function rather than a value
Suppose that there is some existing key function my_key
, and we want to sort descending by its results, then ascending by some other key. Rather than rewriting my_key
, we can adapt it like so:
def negated_result(func):
return lambda x: negated(func(x))
# Which now allows:
data.sort(lambda d: (negated_result(my_key)(d), d.id))
Since negated_result
accepts a function and returns a function, it can also be used as a decorator.
If all else fails: repeated sorting per-key
Since Python's built-in sort is guaranteed stable, we can simply sort on the second key, and then on the first:
# Sort "by my_key descending, then id ascending", by doing the steps
# the other way around.
data.sort(lambda d: d.id)
data.sort(my_key, reverse=True)
The idea is that the sub-ordering will be preserved while applying the main ordering. It's a bit tricky to remember to do this in reverse order, so a wrapper function might be desired. For example:
# Use the `operator` module to avoid writing lambdas for simple accesses.
# This is not much simpler, but arguably more explicit.
from operator import attrgetter
# Give the sort orderings nicer names.
# See: https://stackoverflow.com/questions/31509401
from enum import Flag
class SortOrder(Flag):
DESCENDING = True
ASCENDING = False
def multi_sort(a_list, *specs):
'''Sort by multiple, optionally reversed keys.
specs -> a sequence of (func, bool) tuples.
Each tuple specifies a key func to use for sorting,
and whether or not to reverse the sort.'''
for key, reverse in reversed(specs):
# The enum value must be converted explicitly to work.
a_list.sort(key=key, reverse=bool(reverse))
# Now the same sort looks like:
multi_sort(
data,
(my_key, SortOrder.DESCENDING),
(attrgetter('id'), SortOrder.ASCENDING)
)