Sort list by multiple attributes?

I have a list:

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

If I want to sort by an element (for example, a high / short element), I can use s = sorted(s, key = itemgetter(1)).

If I want to sort by high / short, color, I can sort each element twice, once, but there's a faster way?

#1 building

I'm not sure if this is the most Python method... I have a list of tuples that need to be sorted in descending order for integer values, and then alphabetically for the second. This requires an inverse integer sort, not an alphabetic sort. This is my solution: (in an exam, I didn't even know you could "nest" the sorting function)

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)]

#2 building

The key can be a function that returns a tuple:

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

Alternatively, you can use itemgetter to achieve the same effect (which is faster and avoids Python function calls):

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

And note that you can use sort here instead of sort and reassign:

s.sort(key = operator.itemgetter(1, 2))

#3 building

It seems that you can use list instead of tuple. I think that's especially important when you get the "magic index" of a property rather than a list / tuple.

In my case, I want to sort by multiple properties of the class, where the key passed in is a string. I need to sort differently in different places, and I want to provide a common default sort for the parent class that interacts with the customer. You only need to override the sort keys when you really need them, and you can also store them as a list that your class can share

So first of all, I defined an auxiliary 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 ]

And then 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 to sort the list by object.attrA, then by object.attrB, assuming that the object has a getter corresponding to the provided string name. The second case will be sorted by object.attrC and then object. Attra.

This also allows you to potentially expose out of the box sorting options for fragmentation by users, unit tests, or perhaps just let them tell you a list without telling you how to sort certain operations in the api to couple them to your back-end implementation.

#4 building

This is a method: you basically rewrite your sorting function to get a list of sorting functions. Each sorting function will compare the properties you want to test. In each sorting test, you will check whether the cmp function returns a non-zero return value. If so, interrupt and send the return value. You can call it by calling the Lambda of the Lambda list function.

Its advantage is that it can pass the data one time instead of the previous sorting as other methods. The other thing is that it's sorted in place, and sorting seems to be replicable.

I used it to write a rank function that ranks the list of classes that each object has a score function in a group, but you can add any attribute list. Notice something like an unlambda, although you'll use a lambda to call a setter. The rank part is not available for list arrays, but sorting is OK.

#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)

This is a way to rank the 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

#5 building

The party was a few years late, but I wanted to sort by two criteria at the same time and use reverse=True. If others want to know how to do it, enclose the condition (function) in parentheses:

s = sorted(my_list, key=lambda i: ( criteria_1(i), criteria_2(i) ), reverse=True)

Tags: Lambda Python Attribute

Posted on Sun, 09 Feb 2020 04:58:24 -0500 by Kingw