Python Arena
Step 3 of 4

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))
Python stops with
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)
Output
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)
Output
Leo
Kai
Leo 20
Ann 12
Uma 12
Kai 7

Uma and Ann both have 12 points, so their names decide: Ann comes first.

Your task

Challenge 1 of 3

Fill in the blank so that the movies are listed from the shortest to the longest. The program should print:

Up (96 min)
Alien (117 min)
Titanic (194 min)

Challenge 2

The class Runner holds a runner's name and finishing time in seconds. Write a function fastest(runners) that returns the name of the runner with the smallest time, as a string. For example,

runners = [Runner("Ines", 312.5), Runner("Tom", 298.1), Runner("Sam", 305.0)]
print(fastest(runners))

should print:

Tom

Challenge 3

Write a function leaderboard(players) that returns a new list of the Player objects sorted by points, the most points first. Players with equal points are listed in alphabetical order by name. The class has a __repr__, so a list of players prints readably. For example,

players = [Player("Zed", 21), Player("Lia", 30), Player("Abe", 21), Player("Kim", 5)]
print(leaderboard(players))

should print:

[Lia (30), Abe (21), Zed (21), Kim (5)]