689

I have a list of lists:

[[12, 'tall', 'blue', 1],
[2, 'short', 'red', 9],
[4, 'tall', 'blue', 13]]

If I wanted to sort by one element, say the tall/short element, I could do it via s = sorted(s, key = itemgetter(1)).

If I wanted to sort by both tall/short and colour, I could do the sort twice, once for each element, but is there a quicker way?

Chris_Rands
  • 38,994
  • 14
  • 83
  • 119
headache
  • 8,377
  • 5
  • 18
  • 6
  • [Related PPCG question](http://codegolf.stackexchange.com/q/85321/34718) – mbomb007 Jul 13 '16 at 15:02
  • 35
    If you use **tuples** instead of lists, python orders sorts by entries from left to right when you run `sort`. That is, `sorted([(4, 2), (0, 3), (0, 1)]) == [(0, 1), (0, 3), (4, 2)]`. – Mateen Ulhaq Oct 24 '18 at 22:21
  • 1
    If the sorting order from the two keys are different, refer to: [sorting - sort Python list with two keys but only one in reverse order - Stack Overflow](https://stackoverflow.com/questions/37693373/sort-python-list-with-two-keys-but-only-one-in-reverse-order) or [a comment under the currently accepted answer](https://stackoverflow.com/questions/4233476/sort-a-list-by-multiple-attributes#comment26419462_4233482) – user202729 Aug 14 '21 at 13:42

7 Answers7

1182

A key can be a function that returns a tuple:

s = sorted(s, key = lambda x: (x[1], x[2]))

Or you can achieve the same using itemgetter (which is faster and avoids a Python function call):

import operator
s = sorted(s, key = operator.itemgetter(1, 2))

And notice that here you can use sort instead of using sorted and then reassigning:

s.sort(key = operator.itemgetter(1, 2))
smci
  • 32,567
  • 20
  • 113
  • 146
Mark Byers
  • 811,555
  • 193
  • 1,581
  • 1,452
  • 2
    You learn something new everyday! Do you know if this is computationally quicker than the previous method? Or does it just do the same thing in the background? – headache Nov 20 '10 at 15:35
  • 2
    @headache: I don't know which is faster - I suspect that they are about the same. You can use the `timeit` module to measure the performance of both if you are interested. – Mark Byers Nov 20 '10 at 15:38
  • 28
    For completeness from timeit: for me first gave 6 us per loop and the second 4.4 us per loop – Brian Larsen Feb 08 '13 at 21:52
  • 30
    Is there a way to sort the first one ascending and the second one descending? (Assume both attributes are strings, so no hacks like adding `-` for integers) – Martin Thoma Aug 05 '13 at 11:03
  • 6
    @moose: Unfortunately I don't think there is a non-hack way. The method proposed in the documentation is to sort twice, using the property that sorting is stable. See [here](http://docs.python.org/2/howto/sorting.html#sort-stability-and-complex-sorts) for more details. – Mark Byers Aug 06 '13 at 14:09
  • 97
    how about if I want to apply `revrse=True` only to `x[1]` is that possible ? – Amyth Nov 24 '14 at 11:45
  • 37
    @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.itemgetter(1), reverse=True)` Not ideal, but works. – tomcounsell Apr 13 '15 at 09:43
  • @tomcounsell I think sorting twice is the best strategy in theis scenario as `sorted` is guaranteed to be stable ([source](https://docs.python.org/3/library/functions.html#sorted)) – Martin Thoma Apr 13 '15 at 15:40
  • 73
    @Amyth or another option, if key is number, to make it reverse, you can multiple it by `-1`. – Serge Apr 06 '16 at 11:21
  • 1
    To @Amyth's comment on negating, see related question [here](https://stackoverflow.com/q/46957889/7954504). – Brad Solomon Oct 26 '17 at 15:25
  • i've a list of array and i used this `linesOrderedLeft = lines_Left.sort(key= lambda t: (t[0][1],t[0][3]) )` but it doesn't work with me as i am trying to sort the arrays based on their first and third index by summing them up So what is the problem with my code – sara Jan 29 '18 at 23:11
  • 1
    You should say that `key = operator.itemgetter(...)` is much faster than `key = lambda: ...`, and move it to the top. Also, lambda is unnecessary rather than itemgetter if the custom sort is only accessing multiple fields, and not transforming them (e.g. negation, lowercase, custom order, ASCIIbetical order, a chain of object method calls etc.) – smci Apr 24 '18 at 22:03
  • 1
    @Serge +1 and not only works for int also boolean and string data = sorted(data, key=lambda x: (x['active']*-1,x['name'])) – eyildiz May 08 '18 at 09:31
  • @Amyth another way to use `reverse=True` is to wrap both sorting criteria in parenthesis: `s = sorted(s, key=lambda i: ( criteria1(i), criteria2(i) ), reverse=True)` – ron_g May 21 '19 at 14:13
  • 3
    @eyildiz multiplying by -1 doesn't work for string data – Markus von Broady Dec 22 '19 at 16:10
  • 2
    could someone please explain how using the callable created from functions in the `operator` module avoids a function call compared to using a `lambda`? I mean using something like `operator.itemgetter` is still a function call, right? – Jethro Cao Jun 09 '20 at 19:15
  • I think the difference is that `itemgetter()` is a *compiled* Python library function, but using `lambda` creates a new function that needs to be interpreted. – Mr. Lance E Sloan Aug 25 '22 at 15:41
  • If you want to sort by a boolean value and reverse you can use not, also when sorting by multiple attributes ``k: (not k['has_paid'], k['name'])`` – Nico Müller Jan 03 '23 at 20:02
58

I'm not sure if this is the most pythonic method ... I had a list of tuples that needed sorting 1st by descending integer values and 2nd alphabetically. This required reversing the integer sort but not the alphabetical sort. Here was my solution: (on the fly in an exam btw, I was not even aware you could 'nest' sorted functions)

a = [('Al', 2),('Bill', 1),('Carol', 2), ('Abel', 3), ('Zeke', 2), ('Chris', 1)]  
b = sorted(sorted(a, key = lambda x : x[0]), key = lambda x : x[1], reverse = True)  
print(b)  
[('Abel', 3), ('Al', 2), ('Carol', 2), ('Zeke', 2), ('Bill', 1), ('Chris', 1)]
  • 27
    since 2nd is a number, it works to do it like `b = sorted(a, key = lambda x: (-x[1], x[0]))` which is more visible on which criteria applies first. as for efficiency I'm not sure, someone needs to timeit. – Andrei-Niculae Petre May 17 '17 at 07:21
22

Several years late to the party but I want to both sort on 2 criteria and use reverse=True. In case someone else wants to know how, you can wrap your criteria (functions) in parenthesis:

s = sorted(my_list, key=lambda i: ( criteria_1(i), criteria_2(i) ), reverse=True)
ron_g
  • 1,474
  • 2
  • 21
  • 39
7

It appears you could use a list instead of a tuple. This becomes more important I think when you are grabbing attributes instead of 'magic indexes' of a list/tuple.

In my case I wanted to sort by multiple attributes of a class, where the incoming keys were strings. I needed different sorting in different places, and I wanted a common default sort for the parent class that clients were interacting with; only having to override the 'sorting keys' when I really 'needed to', but also in a way that I could store them as lists that the class could share

So first I defined a helper method

def attr_sort(self, attrs=['someAttributeString']:
  '''helper to sort by the attributes named by strings of attrs in order'''
  return lambda k: [ getattr(k, attr) for attr in attrs ]

then to use it

# would defined elsewhere but showing here for consiseness
self.SortListA = ['attrA', 'attrB']
self.SortListB = ['attrC', 'attrA']
records = .... #list of my objects to sort
records.sort(key=self.attr_sort(attrs=self.SortListA))
# perhaps later nearby or in another function
more_records = .... #another list
more_records.sort(key=self.attr_sort(attrs=self.SortListB))

This will use the generated lambda function sort the list by object.attrA and then object.attrB assuming object has a getter corresponding to the string names provided. And the second case would sort by object.attrC then object.attrA.

This also allows you to potentially expose outward sorting choices to be shared alike by a consumer, a unit test, or for them to perhaps tell you how they want sorting done for some operation in your api by only have to give you a list and not coupling them to your back end implementation.

UpAndAdam
  • 4,515
  • 3
  • 28
  • 46
  • Nice work. What if the attributes should be sorted in different orders? Suppose attrA should be sorted ascending and attrB descending? Is there a quick solution on top of this? Thanks! – mhn_namak Feb 03 '20 at 22:18
  • @mhn_namak see https://stackoverflow.com/a/55866810/2359945 which is a beautiful way to sort on n criteria, each in either ascending or descending. – Razzle Shazl Feb 09 '21 at 13:03
  • We clearly have very different views on beautiful. While it does the job that is fugliest thing I have ever seen. And the efficiency becomes a function of (n*m) where m is number of attributes to sort on instead of just a function of the length of the list. i would think other answers here have better solutions or you could write your own sort function to do it yourself if you really needed that behavior – UpAndAdam Feb 18 '21 at 19:59
6

convert the list of list into a list of tuples then sort the tuple by multiple fields.

 data=[[12, 'tall', 'blue', 1],[2, 'short', 'red', 9],[4, 'tall', 'blue', 13]]

 data=[tuple(x) for x in data]
 result = sorted(data, key = lambda x: (x[1], x[2]))
 print(result)

output:

 [(2, 'short', 'red', 9), (12, 'tall', 'blue', 1), (4, 'tall', 'blue', 13)]
Golden Lion
  • 3,840
  • 2
  • 26
  • 35
2

Here's one way: You basically re-write your sort function to take a list of sort functions, each sort function compares the attributes you want to test, on each sort test, you look and see if the cmp function returns a non-zero return if so break and send the return value. You call it by calling a Lambda of a function of a list of Lambdas.

Its advantage is that it does single pass through the data not a sort of a previous sort as other methods do. Another thing is that it sorts in place, whereas sorted seems to make a copy.

I used it to write a rank function, that ranks a list of classes where each object is in a group and has a score function, but you can add any list of attributes. Note the un-lambda-like, though hackish use of a lambda to call a setter. The rank part won't work for an array of lists, but the sort will.

#First, here's  a pure list version
my_sortLambdaLst = [lambda x,y:cmp(x[0], y[0]), lambda x,y:cmp(x[1], y[1])]
def multi_attribute_sort(x,y):
    r = 0
    for l in my_sortLambdaLst:
        r = l(x,y)
        if r!=0: return r #keep looping till you see a difference
    return r

Lst = [(4, 2.0), (4, 0.01), (4, 0.9), (4, 0.999),(4, 0.2), (1, 2.0), (1, 0.01), (1, 0.9), (1, 0.999), (1, 0.2) ]
Lst.sort(lambda x,y:multi_attribute_sort(x,y)) #The Lambda of the Lambda
for rec in Lst: print str(rec)

Here's a way to rank a list of objects

class probe:
    def __init__(self, group, score):
        self.group = group
        self.score = score
        self.rank =-1
    def set_rank(self, r):
        self.rank = r
    def __str__(self):
        return '\t'.join([str(self.group), str(self.score), str(self.rank)]) 


def RankLst(inLst, group_lambda= lambda x:x.group, sortLambdaLst = [lambda x,y:cmp(x.group, y.group), lambda x,y:cmp(x.score, y.score)], SetRank_Lambda = lambda x, rank:x.set_rank(rank)):
    #Inner function is the only way (I could think of) to pass the sortLambdaLst into a sort function
    def multi_attribute_sort(x,y):
        r = 0
        for l in sortLambdaLst:
            r = l(x,y)
            if r!=0: return r #keep looping till you see a difference
        return r

    inLst.sort(lambda x,y:multi_attribute_sort(x,y))
    #Now Rank your probes
    rank = 0
    last_group = group_lambda(inLst[0])
    for i in range(len(inLst)):
        rec = inLst[i]
        group = group_lambda(rec)
        if last_group == group: 
            rank+=1
        else:
            rank=1
            last_group = group
        SetRank_Lambda(inLst[i], rank) #This is pure evil!! The lambda purists are gnashing their teeth

Lst = [probe(4, 2.0), probe(4, 0.01), probe(4, 0.9), probe(4, 0.999), probe(4, 0.2), probe(1, 2.0), probe(1, 0.01), probe(1, 0.9), probe(1, 0.999), probe(1, 0.2) ]

RankLst(Lst, group_lambda= lambda x:x.group, sortLambdaLst = [lambda x,y:cmp(x.group, y.group), lambda x,y:cmp(x.score, y.score)], SetRank_Lambda = lambda x, rank:x.set_rank(rank))
print '\t'.join(['group', 'score', 'rank']) 
for r in Lst: print r
0

There is a operator < between lists e.g.:

[12, 'tall', 'blue', 1] < [4, 'tall', 'blue', 13]

will give

False
baz
  • 1,317
  • 15
  • 10