Сортировка списка в Python: sorted, key и reverse

Python Автор: Среда и версия: CPython 3.14.5
содержание

Сортировка в Python настраивается одним аргументом. key принимает функцию, которая для каждого элемента возвращает значение для сравнения, и этого хватает почти на все задачи: по длине, по полю словаря, по нескольким уровням сразу. Разница между sorted() и методом sort() разобрана в статье про списки, здесь всё остальное. Код выполнен на CPython 3.14.5.

Дальше на одном наборе:

Python / 01

key вычисляет ключ, но возвращаются исходные элементы

key вычисляет ключ, но возвращаются исходные элементы01 / вход 02 / операция 03 / результат Уфа len = 3 1-е место Сочи len = 4 2-е место Москва len = 6 3-е место Санкт-Петербург len = 15 4-е место01 / вход02 / операция03 / результатУфаlen = 31-е местоСочиlen = 42-е местоМоскваlen = 63-е местоСанкт-Петербургlen = 154-е место
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 по значению, порядок внутри группы, устойчивость при повторных ключах.

Источники