Sort a list by multiple attributes?
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?
A key can be a function that returns a tuple:
s = sorted(s, key = lambda x: (x, x))
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))
Read more... Read less...
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), key = lambda x : x, reverse = True) print(b) [('Abel', 3), ('Al', 2), ('Carol', 2), ('Zeke', 2), ('Bill', 1), ('Chris', 1)]
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)
It appears you could use a
list instead of a
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 has a getter corresponding to the string names provided. And the second case would sort by
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.
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, y), lambda x,y:cmp(x, y)] 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) 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
There is a operator < between lists e.g.:
[12, 'tall', 'blue', 1] < [4, 'tall', 'blue', 13]