Sorting your own objects
Lists often hold objects of your own classes. Python doesn't know how to compare two such objects, so sorted on its own stops with an error:
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
books = [Book("Dune", "Frank Herbert", 1965), Book("Emma", "Jane Austen", 1815)]
print(sorted(books))
Traceback (most recent call last):
File "main.py", line 9, in <module>
print(sorted(books))
~~~~~~^^^^^^^
TypeError: '<' not supported between instances of 'Book' and 'Book'Sorting compares the items with <, and a Book has no <. You could add one with the operator overloading of part 10, but a book has no single natural order anyway: by title, by author or by year all make sense. A key function lets you choose, every time you sort.
A key that reads an attribute
The key function gets one object and returns the attribute to sort by. A named function and a lambda work equally well:
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __str__(self):
return f"{self.title} ({self.author}, {self.year})"
def by_year(book):
return book.year
books = [
Book("Dune", "Frank Herbert", 1965),
Book("Emma", "Jane Austen", 1815),
Book("Beloved", "Toni Morrison", 1987),
]
print("By year:")
for book in sorted(books, key=by_year):
print(book)
print("By title:")
for book in sorted(books, key=lambda book: book.title):
print(book)
By year:
Emma (Jane Austen, 1815)
Dune (Frank Herbert, 1965)
Beloved (Toni Morrison, 1987)
By title:
Beloved (Toni Morrison, 1987)
Dune (Frank Herbert, 1965)
Emma (Jane Austen, 1815)A key can call a method too, such as key=lambda shape: shape.area(), when the value comes from a method instead of an attribute.
Best, worst and ties
min and max with a key return the whole object, so you can use any of its attributes afterwards. And when two objects have the same value, a key can return a tuple to settle the tie: tuples are compared item by item, so the second item counts only when the first items are equal. To sort a number from highest to lowest but names from A to Z, make the number negative in the key:
class Player:
def __init__(self, name, points):
self.name = name
self.points = points
players = [Player("Uma", 12), Player("Leo", 20), Player("Ann", 12), Player("Kai", 7)]
print(max(players, key=lambda player: player.points).name)
print(min(players, key=lambda player: player.points).name)
for player in sorted(players, key=lambda player: (-player.points, player.name)):
print(player.name, player.points)
Leo
Kai
Leo 20
Ann 12
Uma 12
Kai 7Uma and Ann both have 12 points, so their names decide: Ann comes first.