Список чисел или строк сортируется сам: sorted([3, 1, 2]). А список товаров — нет: Python не знает, какой
товар «меньше» другого.
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__, — поэтому > тоже заработал.
Допишите класс Drink: напиток «меньше» другого, если у него меньше объём volume; сравнение с чужим типом —
NotImplemented. После этого сохраните в ordered список напитков из menu, отсортированный по объёму,
а в biggest — самый большой напиток.
__lt__ устроен как __eq__: сначала тип other, потом сравнение нужного атрибута.ordered = sorted(menu), biggest = max(menu) — они сами вызовут __lt__.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
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 = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
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):
if not isinstance(other, Drink):
return NotImplemented
return self.volume < other.volume
menu = [Drink("Латте", 400), Drink("Эспрессо", 60), Drink("Капучино", 300)]
ordered = sorted(menu)
biggest = max(menu)@total_ordering: все шесть сравнений
Сравнений шесть: ==, !=, <, >, <=, >=. != получается из __eq__, > — из __lt__ второго
объекта, а вот <= и >= сами не появятся:
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
Объявите класс Booking с декоратором @total_ordering: Booking("Анна", "18:30") хранит имя гостя name
и время time строкой «ЧЧ:ММ». Брони сравниваются по времени, а при одинаковом времени — по имени: объявите
__eq__ и __lt__ (для чужого типа — NotImplemented), остальные сравнения достроит декоратор.
(self.time, self.name) < (other.time, other.name) — кортежи сравниваются по первому элементу, при равенстве — по второму."09:30" < "18:00".Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
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):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
from functools import total_ordering
@total_ordering
class Booking:
def __init__(self, name, time):
self.name = name
self.time = time
def __eq__(self, other):
if not isinstance(other, Booking):
return NotImplemented
return (self.time, self.name) == (other.time, other.name)
def __lt__(self, other):
if not isinstance(other, Booking):
return NotImplemented
return (self.time, self.name) < (other.time, other.name)key=: порядок снаружи
Не у каждого класса есть один естественный порядок. Товары сортируют то по цене, то по названию, то по
популярности — и ни один порядок не «главный». Тогда в классе ничего не объявляют, а передают sorted, min
и max параметр 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,
только в одну строку и прямо на месте аргумента.
У класса Product из демонстрации нет сравнений. Для списка products сохраните в by_name названия товаров
по алфавиту, в by_price_desc — названия от самого дорогого к самому дешёвому, а в cheapest — сам самый
дешёвый товар.
sorted(products, key=lambda p: p.name) даёт товары — названия соберите генератором списка.reverse=True; самый дешёвый — min(products, key=lambda p: p.price).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
products = [Product("Раф", 260), Product("Чай", 120), Product("Латте", 220), Product("Какао", 190)]
by_name = ...
by_price_desc = ...
cheapest = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
products = [Product("Раф", 260), Product("Чай", 120), Product("Латте", 220), Product("Какао", 190)]
by_name = [p.name for p in sorted(products, key=lambda p: p.name)]
by_price_desc = [p.name for p in sorted(products, key=lambda p: p.price, reverse=True)]
cheapest = min(products, key=lambda p: p.price)Кофейня печатает меню то по цене, то по калорийности, то по алфавиту. Как лучше сортировать напитки?
__lt__ задаёт один естественный порядок — его стоит объявлять, когда он очевиден (время брони, номер версии). Когда порядков несколько и ни один не главный, честнее key=: порядок виден прямо в месте сортировки. Три метода __lt__ в классе невозможны — останется последний.