pythonsortingdictionary

How to sort objects by multiple keys?


Or, practically, how can I sort a list of dictionaries by multiple keys?

I have a list of dicts:

b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

and I need to use a multi key sort reversed by Total_Points, then not reversed by TOT_PTS_Misc.

This can be done at the command prompt like so:

a = sorted(b, key=lambda d: (-d['Total_Points'], d['TOT_PTS_Misc']))

But I have to run this through a function, where I pass in the list and the sort keys. For example, def multikeysort(dict_list, sortkeys):.

How can the lambda line be used which will sort the list, for an arbitrary number of keys that are passed in to the multikeysort function, and take into consideration that the sortkeys may have any number of keys and those that need reversed sorts will be identified with a '-' before it?


Solution

  • This answer works for any kind of column in the dictionary -- the negated column need not be a number.

    def multikeysort(items, columns):
        from operator import itemgetter
        comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else
                      (itemgetter(col.strip()), 1)) for col in columns]
        def comparer(left, right):
            for fn, mult in comparers:
                result = cmp(fn(left), fn(right))
                if result:
                    return mult * result
            else:
                return 0
        return sorted(items, cmp=comparer)
    

    You can call it like this:

    b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
         {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
         {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
         {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
         {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
         {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
         {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
         {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
         {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
         {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
         {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
         {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
         {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
         {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
         {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
         {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
         {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
         {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
         {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
         {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
         {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
         {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]
    
    a = multikeysort(b, ['-Total_Points', 'TOT_PTS_Misc'])
    for item in a:
        print item
    

    Try it with either column negated. You will see the sort order reverse.

    Next: change it so it does not use extra class....


    2016-01-17

    Taking my inspiration from this answer What is the best way to get the first item from an iterable matching a condition?, I shortened the code:

    from operator import itemgetter as i
    
    def multikeysort(items, columns):
        comparers = [
            ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
            for col in columns
        ]
        def comparer(left, right):
            comparer_iter = (
                cmp(fn(left), fn(right)) * mult
                for fn, mult in comparers
            )
            return next((result for result in comparer_iter if result), 0)
        return sorted(items, cmp=comparer)
    

    In case you like your code terse.


    Later 2016-01-17

    This works with python3 (which eliminated the cmp argument to sort):

    from operator import itemgetter as i
    from functools import cmp_to_key
    
    def cmp(x, y):
        """
        Replacement for built-in function cmp that was removed in Python 3
    
        Compare the two objects x and y and return an integer according to
        the outcome. The return value is negative if x < y, zero if x == y
        and strictly positive if x > y.
    
        https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function
        """
    
        return (x > y) - (x < y)
    
    def multikeysort(items, columns):
        comparers = [
            ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
            for col in columns
        ]
        def comparer(left, right):
            comparer_iter = (
                cmp(fn(left), fn(right)) * mult
                for fn, mult in comparers
            )
            return next((result for result in comparer_iter if result), 0)
        return sorted(items, key=cmp_to_key(comparer))
    

    Inspired by this answer How should I do custom sort in Python 3?