Нашли ошибку?
Скачать чистый
урок 3 из 4≈ 20 минупражнения: 0 из 3

Сортировка объектов

__lt__ учит объекты сравниваться на «меньше» — и начинают работать sorted, min и max. @total_ordering дописывает остальные сравнения, а key= сортирует без магических методов.

Python запустится при первом выполнении

Список чисел или строк сортируется сам: sorted([3, 1, 2]). А список товаров — нет: Python не знает, какой товар «меньше» другого.

[ ]Товары не сортируютсяошибка: TypeError
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __repr__(self):
        return f"Product({self.name!r}, {self.price})"


products = [Product("Раф", 260), Product("Чай", 120), Product("Латте", 220)]
sorted(products)
Ошибкасохранённый · нажмите «Выполнить», чтобы получить свой
TypeError: '<' not supported between instances of 'Product' and 'Product'

Есть два способа научить: объявить «естественный порядок» в самом классе или передать порядок в sorted снаружи — параметром key.

__lt__: «меньше»

a < b вызывает a.__lt__(b) (lt — от less than). Этого одного метода достаточно для sorted, min, max и метода списка sort: sorted, sort и min сравнивают через <, а max — через >, который Python сводит к < второго объекта (подробнее — сразу под демонстрацией):

[ ]Товары по цене
class PricedProduct:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __repr__(self):
        return f"PricedProduct({self.name!r}, {self.price})"

    def __lt__(self, other):
        if not isinstance(other, PricedProduct):
            return NotImplemented
        return self.price < other.price


shelf = [PricedProduct("Раф", 260), PricedProduct("Чай", 120), PricedProduct("Латте", 220)]
print(sorted(shelf))
print(min(shelf), max(shelf))
print(shelf[0] > shelf[1])  # Python спросил shelf[1] < shelf[0]
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
[PricedProduct('Чай', 120), PricedProduct('Латте', 220), PricedProduct('Раф', 260)]
PricedProduct('Чай', 120) PricedProduct('Раф', 260)
True

Для чужого типа __lt__, как и __eq__, возвращает NotImplemented. А a > b Python сам превращает в b < a, если у a нет своего __gt__, — поэтому > тоже заработал.

[ ]Упражнение 1Напитки по объёму решено

Допишите класс Drink: напиток «меньше» другого, если у него меньше объём volume; сравнение с чужим типом — NotImplemented. После этого сохраните в ordered список напитков из menu, отсортированный по объёму, а в biggest — самый большой напиток.

class Drink:
    def __init__(self, name, volume):
        self.name = name
        self.volume = volume

    def __repr__(self):
        return f"Drink({self.name!r}, {self.volume})"

    def __lt__(self, other):
        ...


menu = [Drink("Латте", 400), Drink("Эспрессо", 60), Drink("Капучино", 300)]
ordered = ...
biggest = ...

@total_ordering: все шесть сравнений

Сравнений шесть: ==, !=, <, >, <=, >=. != получается из __eq__, > — из __lt__ второго объекта, а вот <= и >= сами не появятся:

[ ]<= не работаетошибка: TypeError
shelf[0] <= shelf[1]
Ошибкасохранённый · нажмите «Выполнить», чтобы получить свой
TypeError: '<=' not supported between instances of 'PricedProduct' and 'PricedProduct'

Писать ещё два метода не нужно. Декоратор total_ordering из модуля functools стандартной библиотеки достраивает недостающие сравнения из __eq__ и __lt__. Декоратор класса ставится над строкой class:

[ ]Все сравнения из двух методов
from functools import total_ordering


@total_ordering
class Version:
    def __init__(self, major, minor):
        self.major = major
        self.minor = minor

    def __repr__(self):
        return f"Version({self.major}, {self.minor})"

    def __eq__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return (self.major, self.minor) == (other.major, other.minor)

    def __lt__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return (self.major, self.minor) < (other.major, other.minor)


old, new = Version(1, 9), Version(2, 0)
print(old < new, old <= new, old >= new, old != new)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
True True False True
[ ]Упражнение 2Бронирование столиков решено

Объявите класс Booking с декоратором @total_ordering: Booking("Анна", "18:30") хранит имя гостя name и время time строкой «ЧЧ:ММ». Брони сравниваются по времени, а при одинаковом времени — по имени: объявите __eq__ и __lt__ (для чужого типа — NotImplemented), остальные сравнения достроит декоратор.

from functools import total_ordering


class Booking:
    def __init__(self, name, time):
        self.name = name
        self.time = time

    def __eq__(self, other):
        ...

    def __lt__(self, other):
        ...

key=: порядок снаружи

Не у каждого класса есть один естественный порядок. Товары сортируют то по цене, то по названию, то по популярности — и ни один порядок не «главный». Тогда в классе ничего не объявляют, а передают sorted, min и max параметр key — функцию, которая для каждого элемента возвращает значение, по которому сравнивать:

[ ]Сортировка с key
def price_of(p):
    return p.price


print(sorted(products, key=price_of))
print(sorted(products, key=lambda p: p.price))  # то же самое
print(max(products, key=lambda p: len(p.name)))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
[Product('Чай', 120), Product('Латте', 220), Product('Раф', 260)]
[Product('Чай', 120), Product('Латте', 220), Product('Раф', 260)]
Product('Латте', 220)

lambda p: p.price — короткая безымянная функция (lambda): то же самое, что def price_of(p): return p.price, только в одну строку и прямо на месте аргумента.

[ ]Упражнение 3Прайс по-разному решено

У класса Product из демонстрации нет сравнений. Для списка products сохраните в by_name названия товаров по алфавиту, в by_price_desc — названия от самого дорогого к самому дешёвому, а в cheapest — сам самый дешёвый товар.

products = [Product("Раф", 260), Product("Чай", 120), Product("Латте", 220), Product("Какао", 190)]
by_name = ...
by_price_desc = ...
cheapest = ...

Вопрос

Кофейня печатает меню то по цене, то по калорийности, то по алфавиту. Как лучше сортировать напитки?

Нашли ошибку?