Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 16 из 20 Средний 35 мин 110 XP

Сортировка и топ-N: sorted по ключу

Список из API приходит в том порядке, в каком отдал сервер: sorted с key наводит порядок, reverse разворачивает, срез берёт топ-3.

Редакция Питоники

Фильтр из прошлого урока отобрал подходящие товары — но порядок в выборке остался таким, каким его отдал сервер. А людям подавай порядок: каталог — по возрастанию цены, витрина — по убыванию рейтинга, отчёт — с топ-3 во главе. За всё это отвечает одна стандартная функция — sorted, и в этом уроке ты поставишь её на поток.

Всё, как обычно в разделе, исполняется прямо на странице: сохранённый ответ API лежит в константе, sorted обрабатывает его без всякой сети.

sorted по ключу: сортируем по полю

sorted принимает список и параметр key — функцию, которая для каждого элемента говорит, с чем именно сравнивать. Для списка словарей это почти всегда lambda, достающая поле.

по возрастанию цены
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

by_price = sorted(items, key=lambda item: item["price"])

for item in by_price:
    print(item["name"], "-", item["price"])
Вывод
Мышь - 1290
Веб-камера - 2450
Клавиатура - 4990
Наушники - 7350
Монитор - 18400

Разбираем запись. lambda item: item["price"] — маленькая безымянная функция: получает словарь, возвращает цену. sorted вызывает её для каждого элемента и расставляет элементы по этим значениям: число вызовов равно числу элементов, а сравнивает она уже готовые значения. Исходный items не тронут — sorted построила новый список, точно как фильтр в прошлом уроке строил выборку.

Lambda — не единственный вариант: key принимает любую функцию, которая умеет превращать элемент в значение для сравнения. Если правило сложнее одного обращения к полю — выноси его в обычную функцию с именем: читаемость дороже экономии строки.

key принимает и именованную функцию
def by_price_then_rating(item):
    if item["in_stock"]:
        return (0, item["price"], -item["rating"])
    return (1, item["price"], -item["rating"])

# items - список словарей товаров из ответа API
# top = sorted(items, key=by_price_then_rating)[:3]
Фрагмент без запуска: items здесь не создан. Смысл — в форме: правило сортировки со слоями логики живёт в именованной функции с говорящим именем, а lambda остаётся для правил в одну строку.
по убыванию: reverse=True
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

top_prices = sorted(items, key=lambda item: item["price"], reverse=True)

for item in top_prices:
    print(item["name"], "-", item["price"])
Вывод
Монитор - 18400
Наушники - 7350
Клавиатура - 4990
Веб-камера - 2450
Мышь - 1290

reverse=True переворачивает порядок: от дорогого к дешёвому. Тот же параметр работает и для рейтинга — витрина «от лучшего к худшему» пишется одной строкой. Направление — это часть вопроса, а не отдельный навык: какой порядок делает отчёт понятнее, такой и выбирай. Списки-числа, кстати, сортируются без всякого key: sorted([3, 1, 2]) — уже [1, 2, 3]; key нужен там, где элементы сложные.

Топ-3: срез после сортировки

Топ-N — это сортировка по убыванию плюс срез: сначала расставили по местам, потом взяли первые N. Сортировка не угадывает, что тебе нужно: без key она честно сравнивает словари целиком и падает с ошибкой. Поэтому key здесь обязателен.

топ-3 самых дорогих
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

top3 = sorted(items, key=lambda item: item["price"], reverse=True)[:3]

for place, item in enumerate(top3, start=1):
    print(place, "-", item["name"], "-", item["price"], "руб.")
Вывод
1 - Монитор - 18400 руб.
2 - Наушники - 7350 руб.
3 - Клавиатура - 4990 руб.

Срез [:3] забрал первые три элемента уже отсортированного списка — топ-5 пишется как [:5], топ-10 как [:10]. enumerate с start=1 пронумеровал места с единицы, а не с нуля: в отчёте первое место должно быть первым. Анти-топ считается зеркально: без reverse сортировка по возрастанию ставит дешёвых в начало, и тот же срез [:3] собрал бы «три самых доступных» — направление задавать до среза, а не после.

так падает сортировка без key
items = [
    {"name": "Мышь", "price": 1290},
    {"name": "Коврик", "price": 890},
]

sorted(items)
# TypeError: '<' not supported between instances of 'dict' and 'dict'
Блок не запускается намеренно: код действительно падает. Это поведение настоящего Python, а не ограничение песочницы — добавь key=lambda item: item["price"], и сортировка заработает.

Строки и регистр: куда девается кириллица

Сортировать можно и по строковым полям — по названию, например. Но строки сравниваются по кодам символов, а в Unicode все заглавные буквы стоят раньше строчных: К с кодом 1050, строчное к — с кодом 1082. Отсюда сюрприз: слова с большой буквы оказываются впереди всех слов с маленькой, даже если по алфавиту должны быть в конце.

регистр ломает алфавит
names = ["монитор", "Клавиатура", "Мышь", "наушники"]

print(sorted(names))
print(sorted(names, key=str.lower))
Вывод
['Клавиатура', 'Мышь', 'монитор', 'наушники']
['Клавиатура', 'монитор', 'Мышь', 'наушники']

Первая сортировка поставила оба слова с маленькой буквы в конец — по кодам всё честно, по алфавиту нет. key=str.lower сравнивает элементы в нижнем регистре: сортировка становится человеческой, а исходные строки в списке остаются как были — key меняет только способ сравнения, не сами данные. Приём универсален для любых строковых полей: названия, авторы, города — везде сравнивай в нижнем регистре, храни как есть.

Тот же приём работает и при сортировке товаров по названию — key комбинирует поле и lower: sorted(items, key=lambda item: item["name"].lower()). И ещё одна ловушка ждёт там, где цены приехали строками: строки сравниваются посимвольно, и «18400» окажется меньше «2450», потому что единица идёт раньше двойки. Числам — численное сравнение, строкам — лексикографическое.

строковые цены сортируются неправильно
raw = [
    {"name": "Монитор", "price": "18400"},
    {"name": "Веб-камера", "price": "2450"},
]

by_text = sorted(raw, key=lambda item: item["price"])
by_number = sorted(raw, key=lambda item: float(item["price"]))

print([i["price"] for i in by_text])
print([i["price"] for i in by_number])
Вывод
['18400', '2450']
['2450', '18400']

Первая строка отсортировала строки как словарь для кроссворда: по первым символам. Вторая сначала превратила цены в числа — и порядок стал настоящим. Вывод для практики: перед сортировкой приведи поля к нужным типам, как перед сравнением в фильтре из урока 15 — это одна и та же граница «текст-данные».

Стабильность: кто первый при равных

Что будет с элементами с одинаковым ключом — два товара по одной цене, два заказа с одним сроком? Ничего страшного: сортировка в Python стабильна, при равных ключах она сохраняет исходный порядок. Кто раньше стоял в списке — тот и раньше оказался в отсортированном, а порядок, в котором сервер прислал товары, обычно и есть порядок витрины.

равные ключи сохраняют порядок
orders = [
    {"id": 101, "days": 3},
    {"id": 102, "days": 1},
    {"id": 103, "days": 3},
]

by_age = sorted(orders, key=lambda order: order["days"])

print([order["id"] for order in by_age])
Вывод
[102, 101, 103]

Заказы 101 и 103 простаивали одинаково — и остались в исходном порядке: 101 перед 103. Стабильность делает возможным приём «сортировка по двум полям»: сначала отсортировать по вторичному ключу, потом по основному — равные по основному сохранят порядок вторичного.

Есть путь и короче: key может возвращать кортеж — тогда сравнение идёт по элементам кортежа слева направо, и обе сортировки происходят за один проход. Хитрость с булевым полем: not item["in_stock"] даёт False (ноль) для товаров в наличии, поэтому они оказываются в начале списка.

два ключа разом: кортеж в key
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

by_stock_price = sorted(
    items,
    key=lambda item: (not item["in_stock"], item["price"]),
)

for item in by_stock_price:
    print(item["name"], "-", item["price"], "-", item["in_stock"])
Вывод
Мышь - 1290 - True
Клавиатура - 4990 - True
Наушники - 7350 - True
Веб-камера - 2450 - False
Монитор - 18400 - False

Сначала все товары в наличии по возрастанию цены, следом недоступные — точно так, как сортирует карточки настоящий магазин. Кортеж в key — самый экономный способ выразить «сначала по этому, потом по тому»: не два вызова sorted, а один с парой критериев. Ловушка одна: элементы кортежа должны быть сравнимы между собой — числа с числами, строки со строками; смешивать типы в одном элементе кортежа нельзя.

sorted против sort

У списков есть и метод sort() — сортирует на месте. Разница в двух вещах: sorted возвращает новый список, sort() меняет существующий и возвращает None.

новый список или на месте
nums = [3, 1, 2]

copy = sorted(nums)
print("sorted вернул:", copy)
print("исходный цел:", nums)

nums.sort()
print("после sort():", nums)
print("sort вернул:", nums.sort())
Вывод
sorted вернул: [1, 2, 3]
исходный цел: [3, 1, 2]
после sort(): [1, 2, 3]
sort вернул: None

Для данных из API выбирай sorted: ответ приходит один раз, а вопросов к нему бывает много — у каждого свой отсортированный список, и никто не портит чужой. Метод sort() — для больших списков, когда копия не нужна. И запомни на собеседование: sorted работает с любым итерируемым — кортежем, генератором, — а sort() есть только у списков.

ХочетсяЗапись
по возрастанию поляsorted(items, key=lambda i: i["price"])
по убываниюто же самое + reverse=True
топ-3sorted(..., reverse=True)[:3]
человеческий алфавитsorted(names, key=str.lower)
изменить список на местеitems.sort(key=...) — вернёт None

Финал: фильтр плюс сортировка

в наличии — от дешёвых к дорогим
import json

API_RESPONSE = """
{
  "status": "ok",
  "source": "shop-api",
  "items": [
    {"name": "Клавиатура", "price": 4990, "rating": 4.7, "in_stock": true},
    {"name": "Мышь", "price": 1290, "rating": 4.3, "in_stock": true},
    {"name": "Монитор", "price": 18400, "rating": 4.8, "in_stock": false},
    {"name": "Наушники", "price": 7350, "rating": 3.9, "in_stock": true},
    {"name": "Веб-камера", "price": 2450, "rating": 4.1, "in_stock": false}
  ]
}
"""

items = json.loads(API_RESPONSE)["items"]

available = [item for item in items if item["in_stock"]]
by_price = sorted(available, key=lambda item: item["price"])

for item in by_price:
    print(item["name"], "-", item["price"])
Вывод
Мышь - 1290
Клавиатура - 4990
Наушники - 7350

Две строки — две операции из прошлых уроков: фильтр отобрал доступное, sorted навёл порядок. Именно из таких шагов собирается любой пайплайн обработки ответа API, и финальный проект урока 20 будет именно этим приёмом в масштабе. Обрати внимание и на экономию: сортировать пять товаров или тридцать тысяч — запись не меняется, а срез [:3] на большом списке не станет медленнее — sorted всё равно пройдёт по списку один раз.

Что дальше

Порядок наведён: по цене, по рейтингу, с топами и человеческим алфавитом — и все варианты выросли из одной функции с параметром key. Осталось последнее — результат живёт только до конца скрипта, а отчёт положено сохранять. Следующий урок записывает итоговые данные в json-файл с ensure_ascii=False и indent=2 — и проверяет себя обратным чтением. А про сортировку уже после получения ответа из сети рассказывает финальный проект курса requests.

Топ-N — это не отдельная функция, а привычка: sorted с key ставит элементы по местам, срез забирает нужное количество.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

nums = [3, 1, 2]
copy = sorted(nums)
print(nums, copy)
words = ["банан", "Арбуз", "вишня"]
print(sorted(words, key=str.lower))
data = [{"n": "x", "v": 2}, {"n": "y", "v": 1}]
print(sorted(data, key=lambda d: d["v"])[0]["n"])
Проверь себя
0 / 6

1. Что делает sorted(items, key=lambda item: item["price"]) с исходным списком?

2. Как отсортировать товары от дорогого к дешёвому?

3. Зачем в sorted нужен key для списка словарей?

4. Как из отсортированного по убыванию списка взять топ-3?

5. Почему sorted(names) поставила «Мышь» раньше «монитор»?

6. Что произойдёт с двумя товарами одинаковой цены при сортировке?

Карточки терминов
Запомнено: 0 / 7
Практика

Собери витрину «выбор редакции»: отсортируй товары из сохранённого ответа по рейтингу от высокого к низкому, возьми первые три и выведи каждое место строкой вида «1 - Монитор - 4.8».

practice.py
Вопросы и ответы по уроку

Как взять три самых дешёвых товара?

Сортировать по возрастанию и взять срез: sorted(items, key=lambda i: i["price"])[:3]. Никакого reverse не нужно — возрастание уже ставит дешёвых в начало, и срез забирает первых трёх. Проверяй направление до среза: срез после неправильного направления отдаст не тех, а ошибка не поднимет ни флага.

Как отсортировать список словарей по значению поля в Python?

Функцией sorted с параметром key: sorted(items, key=lambda item: item["price"]). key получает один словарь и возвращает поле для сравнения. Для убывания добавьте reverse=True. Исходный список не меняется — sorted возвращает новый.

Как взять топ-3 самых дорогих товаров из данных API?

Отсортируйте по цене по убыванию и возьмите срез: sorted(items, key=lambda i: i["price"], reverse=True)[:3]. Срез применяется после сортировки, поэтому первые три элемента — действительно лидеры. Для топ-10 срез меняется на [:10].

Почему Python сортирует русские строки не по алфавиту?

Потому что сравнивает коды символов, а заглавные кириллические буквы в Unicode идут раньше строчных: «Мышь» обгоняет «монитор». Лечится параметром key=str.lower — сравнение идёт в нижнем регистре, а исходные строки не меняются.

Чем sorted отличается от list.sort?

sorted(x) — стандартная функция: возвращает новый отсортированный список, не трогая исходный. x.sort() — метод списка: сортирует на месте и возвращает None. Для обработки данных из API удобнее sorted: один ответ, много разных сортировок без побочных эффектов.

Как отсортировать сначала по наличию, потом по цене?

Передать в key кортеж: sorted(items, key=lambda i: (not i["in_stock"], i["price"])). Элементы сравниваются по кортежу слева направо: False (ноль) товаров в наличии оказывается раньше, а внутри групп работает цена. Альтернатива — два последовательных sorted от частного ключа к общему: сработает благодаря стабильности сортировки.

Понравился урок? Сошлитесь на него

«Сортировка не угадывает, что тебе нужно: без key она честно сравнивает словари целиком и падает с ошибкой.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.