Сортировка в Python настраивается одним аргументом. key принимает функцию, которая для каждого элемента возвращает значение для сравнения, и этого хватает почти на все задачи: по длине, по полю словаря, по нескольким уровням сразу. Разница между sorted() и методом sort() разобрана в статье про списки, здесь всё остальное. Код выполнен на CPython 3.14.5.
Дальше на одном наборе:
key вычисляет ключ, но возвращаются исходные элементы
key=len не заменяет города числами. Длина нужна только для сравнения, а в новый список попадают исходные строки.users = [
{"name": "Аня", "city": "Москва", "orders": 3},
{"name": "Борис", "city": "Казань", "orders": 2},
{"name": "Вера", "city": "Москва", "orders": 0},
{"name": "Глеб", "city": "Казань", "orders": 1},
]
print(len(users))
4
Как отсортировать по своему признаку
Без key элементы сравниваются целиком. С key сравнивается то, что вернула функция:
cities = ["Москва", "Сочи", "Санкт-Петербург", "Уфа"]
print(sorted(cities, key=len))
['Уфа', 'Сочи', 'Москва', 'Санкт-Петербург']
Сами города при этом остаются целыми: key влияет только на сравнение, а в результат попадают исходные элементы. Для словарей функцию пишут лямбдой:
users = [
{"name": "Аня", "city": "Москва", "orders": 3},
{"name": "Борис", "city": "Казань", "orders": 2},
{"name": "Вера", "city": "Москва", "orders": 0},
{"name": "Глеб", "city": "Казань", "orders": 1},
]
print([u["name"] for u in sorted(users, key=lambda u: u["orders"])])
['Вера', 'Глеб', 'Борис', 'Аня']
Готовые ключи есть в модуле operator, и на длинных списках они быстрее лямбды, потому что написаны на C:
from operator import itemgetter
users = [
{"name": "Аня", "orders": 3},
{"name": "Борис", "orders": 2},
{"name": "Вера", "orders": 0},
{"name": "Глеб", "orders": 1},
]
print([u["name"] for u in sorted(users, key=itemgetter("orders"))])
['Вера', 'Глеб', 'Борис', 'Аня']
Функция из key вызывается ровно один раз на элемент, а не на каждое сравнение. Это важно, когда ключ считается дорого:
calls = []
def noisy(word):
calls.append(word)
return len(word)
sorted(["аа", "б", "ввв"], key=noisy)
print(len(calls), calls)
3 ['аа', 'б', 'ввв']
Три элемента, три вызова. Кешировать ключ вручную не нужно.
Как сортировать по нескольким полям
key возвращает кортеж, и сравнение идёт по его элементам слева направо:
users = [
{"name": "Аня", "city": "Москва", "orders": 3},
{"name": "Борис", "city": "Казань", "orders": 2},
{"name": "Вера", "city": "Москва", "orders": 0},
{"name": "Глеб", "city": "Казань", "orders": 1},
]
for u in sorted(users, key=lambda u: (u["city"], u["orders"])):
print(u["city"], u["orders"], u["name"])
Казань 1 Глеб
Казань 2 Борис
Москва 0 Вера
Москва 3 Аня
Сначала город по алфавиту, внутри города заказы по возрастанию. Чтобы развернуть только второй уровень, число берут со знаком минус:
users = [
{"name": "Аня", "city": "Москва", "orders": 3},
{"name": "Борис", "city": "Казань", "orders": 2},
{"name": "Вера", "city": "Москва", "orders": 0},
{"name": "Глеб", "city": "Казань", "orders": 1},
]
for u in sorted(users, key=lambda u: (u["city"], -u["orders"])):
print(u["city"], u["orders"], u["name"])
Казань 2 Борис
Казань 1 Глеб
Москва 3 Аня
Москва 0 Вера
Города остались по возрастанию, заказы внутри города перевернулись. Приём работает только для чисел: у строки знака нет, и развернуть текстовый уровень так не выйдет.
Как развернуть текстовый уровень: устойчивость
Для строк вместо минуса используют другое свойство: сортировка в Python устойчива. Элементы с одинаковым ключом сохраняют исходный порядок, поэтому многоуровневую сортировку можно собрать из нескольких проходов, начиная с последнего по важности.
users = [
{"name": "Аня", "city": "Москва", "orders": 2},
{"name": "Борис", "city": "Казань", "orders": 2},
{"name": "Вера", "city": "Москва", "orders": 0},
{"name": "Глеб", "city": "Казань", "orders": 1},
]
by_name = sorted(users, key=lambda u: u["name"], reverse=True)
by_orders = sorted(by_name, key=lambda u: u["orders"])
print([(u["orders"], u["name"]) for u in by_orders])
[(0, 'Вера'), (1, 'Глеб'), (2, 'Борис'), (2, 'Аня')]
Второй проход не разрушил результат первого: у Бориса и Ани по два заказа, поэтому их имена остались в обратном алфавитном порядке. Так сортируют по возрастанию одного поля и убыванию другого, когда минус не применить.
Почему reverse=True и срез дают разное
Оба разворачивают порядок, но по-разному обходятся с одинаковыми ключами:
data = [("б", 1), ("а", 1), ("в", 2)]
print("reverse=True:", sorted(data, key=lambda p: p[1], reverse=True))
print("срез: ", sorted(data, key=lambda p: p[1])[::-1])
reverse=True: [('в', 2), ('б', 1), ('а', 1)]
срез: [('в', 2), ('а', 1), ('б', 1)]
У пары с единицами порядок разный. reverse=True сортирует в обратную сторону, сохраняя устойчивость: «б» стояло раньше «а» во входных данных и осталось раньше. Срез разворачивает уже готовый список целиком, вместе с равными элементами.
На уникальных ключах разницы не будет, поэтому ошибка живёт долго и всплывает на данных с повторами. Правило простое: разворачивать порядок надо аргументом reverse, а не срезом.
Как сортировать словарь
Сам словарь не сортируется, сортируется его снимок. sorted() от словаря даёт ключи:
prices = {"молоко": 90, "хлеб": 60, "сыр": 400}
print(sorted(prices))
print(sorted(prices.items(), key=lambda kv: kv[1]))
print(dict(sorted(prices.items(), key=lambda kv: kv[1])))
['молоко', 'сыр', 'хлеб']
[('хлеб', 60), ('молоко', 90), ('сыр', 400)]
{'хлеб': 60, 'молоко': 90, 'сыр': 400}
Третья строка собирает новый словарь из отсортированных пар. Работает это потому, что словарь хранит порядок вставки. У словаря нет метода sort(): чтобы изменить порядок в том же объекте, пришлось бы удалить записи и вставить их заново.
Как сортировать текст
По умолчанию сравниваются коды символов, поэтому заглавные буквы уходят вперёд всех строчных:
words = ["Яблоко", "арбуз", "Ананас", "банан"]
print(sorted(words))
print(sorted(words, key=str.lower))
['Ананас', 'Яблоко', 'арбуз', 'банан']
['Ананас', 'арбуз', 'банан', 'Яблоко']
Первый ответ выглядит сломанным, но это сравнение по кодам Unicode, а не по правилам языка. Ключ str.lower приводит слова к одному регистру перед сравнением; для этих четырёх слов получается привычный алфавитный порядок.
Та же причина уносит букву «ё» в самый конец: её код выше, чем у «я».
print(sorted(["ёж", "ежевика", "яблоко"]))
print(sorted(["ёж", "ежевика", "яблоко"], key=lambda w: w.replace("ё", "е")))
['ежевика', 'яблоко', 'ёж']
['ёж', 'ежевика', 'яблоко']
Подмена «ё» на «е» в ключе ставит слово на место. Для полноценного алфавитного порядка нужен язык, а не коды символов: в базе данных это правило сравнения, и как оно меняет результат, показано в статье про ORDER BY.
Ошибки, на которых спотыкаются
Смешанные типы. Python не сравнивает строку с числом:
try:
print(sorted([3, "1", 2]))
except TypeError as exc:
print(f"TypeError: {exc}")
TypeError: '<' not supported between instances of 'str' and 'int'
Чаще всего это признак того, что числа пришли из файла или формы как текст. Приводить типы надо до сортировки, а не в ключе.
None среди значений. Ошибка та же по природе:
try:
print(sorted([3, None, 1]))
except TypeError as exc:
print(f"TypeError: {exc}")
TypeError: '<' not supported between instances of 'NoneType' and 'int'
Пропуски отправляют в конец кортежным ключом, где первый элемент — признак пустоты:
values = [3, None, 1]
print(sorted(values, key=lambda v: (v is None, v)))
[1, 3, None]
False меньше True, поэтому заполненные значения идут первыми. Тот же приём в SQL называется NULLS LAST и разобран в статье про NULL.
Присваивание результата sort(). Метод возвращает None, и после nums = nums.sort() имя nums ссылается на None, а не на список. Разбор пары sort и sorted — в статье про списки.
Частые вопросы
Как отсортировать список словарей по значению поля
sorted(users, key=lambda u: u["orders"]). Для нескольких полей ключ возвращает кортеж, а operator.itemgetter("city", "orders") делает то же самое короче.
Как отсортировать по убыванию
Аргументом reverse=True. Срез [::-1] даёт похожий, но другой результат на одинаковых ключах.
Работает ли key у max и min
Да, и у обеих функций он устроен так же: max(users, key=lambda u: u["orders"]) вернёт элемент с наибольшим числом заказов, а не само число.
Как отсортировать по алфавиту с учётом регистра и «ё»
Ключом str.lower можно игнорировать регистр, а заменой «ё» на «е» — считать эти буквы равными. Это упрощение, не полное языковое сравнение. Для правил выбранной локали в стандартной библиотеке есть locale.strxfrm в роли ключа; результат зависит от настроенной локали и её наличия в ОС.
Что учить дальше
Сортировка почти всегда идёт следом за сборкой данных: списки, словари и включения. В пути «Python: структуры данных» на Koddo под сортировку отведён свой пак: топ-N по значению, порядок внутри группы, устойчивость при повторных ключах.