Python 3: Sorting a List of Objects or Dictionaries by Multiple Keys Bidirectionally
Basic Sorting
# Sort a list of dictionary objects by a key - case sensitive
from operator import itemgetter
mylist = sorted(mylist, key=itemgetter('name'))
# Sort a list of dictionary objects by a key - case insensitive
mylist = sorted(mylist, key=lambda k: k['name'].lower())
The above methods for sorting a list of dictionary objects is very common and suitable for sorting lists containing dictionaries or objects. sorted also allows your key function to return a tuple of values if you wish to sort your list by more than one key like so:
Sorting by Multiple Keys
# Sort by multiple keys - case sensitive
from operator import itemgetter
mylist = sorted(mylist, key=itemgetter('name', 'age'))
# Sort by multiple keys - case insensitive
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), k['age']))
Bidirectional Sorting with Numeric Values
Sorting by multiple keys bidirectionally is simple when dealing with numeric values.
from functools import cmp_to_key
from operator import itemgetter, methodcaller
def cmp(a, b):
return (a > b) - (a < b)
def multikeysort(items, columns, functions={}, getter=itemgetter):
"""Sort a list of dictionary objects or objects by multiple keys bidirectionally.
Keyword Arguments:
items -- A list of dictionary objects or objects
columns -- A list of column names to sort by. Use -column to sort in descending order
functions -- A Dictionary of Column Name -> Functions to normalize or process each column value
getter -- Default "getter" if column function does not exist
operator.itemgetter for Dictionaries
operator.attrgetter for Objects
"""
comparers = []
for col in columns:
column = col[1:] if col.startswith('-') else col
if not column in functions:
functions[column] = getter(column)
comparers.append((functions[column], 1 if column == col else -1))
def comparer(left, right):
for func, polarity in comparers:
result = cmp(func(left), func(right))
if result:
return polarity * result
else:
return 0
return sorted(items, key=cmp_to_key(comparer))
def compose(inner_func, *outer_funcs):
"""Compose multiple unary functions together into a single unary function"""
if not outer_funcs:
return inner_func
outer_func = compose(*outer_funcs)
return lambda *args, **kwargs: outer_func(inner_func(*args, **kwargs))
And here are a few examples of them in use:
# We will use a list of dictionary objects representing students, their classes and their current grade
students = [
{'name': 'Paul Allen', 'class': 'Science', 'grade': 'A'},
{'name': 'paul allen', 'class': 'Math', 'grade': 'C'},
{'name': 'Bob Lewis', 'class': 'Science', 'grade': 'D'},
{'name': 'Bob Lewis', 'class': 'math', 'grade': 'b'},
{'name': 'bob Lewis', 'class': 'History', 'grade': 'f'},
]
# Sort by class and grade descending, case sensitive
ex1 = multikeysort(students, ['class', '-grade'])
# Sort by class and grade descending, case insensitive
# Create a unary function that will get the column value and then call the .lower() method
get_class = compose(itemgetter('class'), methodcaller('lower'))
get_grade = compose(itemgetter('grade'), methodcaller('lower'))
ex2 = multikeysort(students, ['class', '-grade'],
{'class': get_class, 'grade': get_grade})