$ sudo teach IT
Модуль 6 · Функции · Урок 6.5

Сортировка с key, enumerate, zip

Как отсортировать данные по нужному признаку и одновременно получить номер каждого элемента или пройтись по двум спискам параллельно

Теория~30 минутНовичокsorted() / key=reverse=enumerate()zip()

Список чисел легко отсортировать: sorted(numbers) расставит их по возрастанию, и вопросов не возникнет. Но в реальных данных редко бывают голые числа. Обычно это список пар "имя, оценка" или список словарей с несколькими полями — и Python должен понять, по какому именно полю сравнивать. Заодно часто нужно не просто получить отсортированный результат, а ещё и пронумеровать его или сопоставить с другим списком. Для этого в Python есть параметр key у sorted() и две вспомогательные функции — enumerate() и zip(). С lambda вы уже знакомы, так что key дастся легко.

sorted() и list.sort(): новый список или перестановка на месте

Представьте, что у вас на столе лежит колода карточек с именами студентов. Есть два способа их упорядочить: переписать имена по алфавиту на новый лист, оставив исходную колоду как была, или прямо перетасовать саму колоду. Оба способа дают отсортированный результат, но по-разному обращаются с оригиналом.

В Python за первый способ отвечает встроенная функция sorted(): она принимает любую последовательность и возвращает новый отсортированный список, а исходные данные не трогает. За второй способ отвечает метод list.sort(): он переставляет элементы прямо внутри списка и ничего не возвращает.

scores = [70, 95, 60, 85]

result = sorted(scores)
print(result)
print(scores)

scores.sort()
print(scores)

Разберём построчно:

  • scores = [70, 95, 60, 85] — список с исходным порядком оценок.
  • result = sorted(scores) — вызываем функцию sorted() и передаём ей список. Она строит новый отсортированный список и возвращает его; мы сохраняем результат в переменную result.
  • print(result) — выведет [60, 70, 85, 95]: новый список отсортирован по возрастанию.
  • print(scores) — выведет [70, 95, 60, 85]: исходный список не изменился, sorted() работает с копией порядка, а не с самим списком.
  • scores.sort() — вызываем метод sort() прямо у списка scores. Он ничего не возвращает (если написать x = scores.sort(), в x окажется None), но переставляет элементы внутри самого списка.
  • print(scores) — теперь выведет [60, 70, 85, 95]: список изменился на месте.

Как выбрать: если исходный порядок ещё понадобится или вы сортируете не список (строку, кортеж, результат другой функции) — берите sorted(). Если оригинал больше не нужен и хочется сэкономить память на копии — .sort().

Параметр key: по какому признаку сравнивать

Пока элементы простые числа, сравнивать их легко: 60 меньше 70, и так далее. Но если каждый элемент — это пара "имя и оценка" или словарь с несколькими полями, Python не знает сам, что именно сравнивать: имена по алфавиту или оценки по числу. Нужно явно объяснить, какую часть каждого элемента брать для сравнения.

Для этого у sorted() (и у .sort()) есть именованный параметр key. Ему передают функцию одного аргумента: для каждого элемента списка Python вызовет эту функцию и будет сравнивать не сами элементы, а то, что функция вернула. Часто эту функцию удобно задать через lambda, которую вы уже разбирали.

students = [("Alice", 85), ("Bob", 92), ("Carol", 78)]

by_score = sorted(students, key=lambda s: s[1])
print(by_score)
  • students = [("Alice", 85), ("Bob", 92), ("Carol", 78)] — список кортежей, у каждого два элемента: имя (индекс 0) и оценка (индекс 1).
  • key=lambda s: s[1] — передаём функцию, которая принимает один элемент списка s (то есть один кортеж вроде ("Alice", 85)) и возвращает s[1] — оценку. Сама функция ничего не сортирует, она только "подсказывает" sorted(), что сравнивать.
  • sorted(students, key=...) — для каждого кортежа Python вызывает эту lambda, получает число (оценку) и сравнивает уже эти числа, а не кортежи целиком.
  • print(by_score) — выведет [('Carol', 78), ('Alice', 85), ('Bob', 92)]: кортежи расставлены по возрастанию оценки, хотя сравнивались не они сами, а вторые элементы каждого из них.

Тот же приём работает и со списком словарей — только вместо индекса кортежа берём значение по ключу словаря:

students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 92},
]

by_grade = sorted(students, key=lambda s: s["grade"])
print(by_grade[0]["name"])
  • key=lambda s: s["grade"] — теперь s на каждом шаге — это один словарь вроде {"name": "Alice", "grade": 85}, а s["grade"] достаёт из него число по ключу "grade".
  • by_grade[0]["name"] — после сортировки берём первый элемент нового списка (студента с самой низкой оценкой) и его поле "name". Выведет Alice.

reverse=True: порядок от большего к меньшему

По умолчанию сортировка всегда идёт от меньшего к большему, будь то числа или буквы алфавита. Часто нужен обратный порядок — например, рейтинг, где на первом месте должен быть лучший результат, а не худший.

Для этого у sorted() есть ещё один именованный параметр — reverse. Он принимает True или False и ничего не меняет в самом сравнении: Python всё так же сравнивает элементы (или результат key), а потом просто разворачивает готовый порядок.

students = [("Alice", 85), ("Bob", 92), ("Carol", 78)]

top = sorted(students, key=lambda s: s[1], reverse=True)
print(top)
  • key=lambda s: s[1] — как и раньше, сравниваем по оценке (второй элемент кортежа).
  • reverse=True — после сравнения по ключу Python выстраивает результат в обратном порядке: от большей оценки к меньшей.
  • print(top) — выведет [('Bob', 92), ('Alice', 85), ('Carol', 78)]: список начинается с лучшего результата.

Совет: key и reverse — это именованные аргументы, их порядок при вызове не важен, важно только писать их название: sorted(data, reverse=True, key=...) сработает так же, как и в обратном порядке.

enumerate(): получить номер вместе со значением

Когда вы перебираете список в цикле for, вы получаете сами значения, но не их порядковый номер. Если для рейтинга или таблицы нужно вывести ещё и номер места — "1. Bob", "2. Alice" — приходится заводить отдельный счётчик и вручную увеличивать его на каждом шаге. Это лишний код и лишний шанс ошибиться.

Встроенная функция enumerate() решает это за вас: она оборачивает любую последовательность так, что на каждом шаге цикла вы получаете пару — номер и значение. По умолчанию нумерация начинается с 0, но можно задать другое начало через параметр start.

names = ["Bob", "Alice", "Carol"]

for i, name in enumerate(names, start=1):
    print(f"{i}. {name}")
  • enumerate(names, start=1) — оборачивает список names и на каждом шаге отдаёт пару (номер, элемент). Параметр start=1 говорит начать нумерацию с 1, а не с 0.
  • for i, name in ... — цикл сразу распаковывает каждую пару в две переменные: i получает номер, name — сам элемент списка.
  • print(f"{i}. {name}") — собирает строку из номера и имени.

Программа выведет:

1. Bob
2. Alice
3. Carol

zip(): два списка бок о бок

Иногда данные приходят не одним списком пар, а двумя отдельными списками: список имён и список оценок, где первое имя относится к первой оценке, второе — ко второй и так далее. Перебирать их синхронно по индексу через range(len(...)) можно, но громоздко и легко ошибиться в индексах.

Функция zip() берёт несколько последовательностей и на каждом шаге отдаёт кортеж из их элементов с одинаковой позицией: первый элемент первой последовательности с первым элементом второй, второй со вторым, и так далее. Название — от англ. "молния" (застёжка), как раз по принципу "зубчик к зубчику".

names = ["Alice", "Bob", "Carol"]
grades = [85, 92, 78]

pairs = list(zip(names, grades))
print(pairs)
  • zip(names, grades) — соединяет два списка по позиции: "Alice" с 85, "Bob" с 92, "Carol" с 78. Сам по себе zip() возвращает не список, а особый объект, который отдаёт пары по одной за раз.
  • list(...) — оборачиваем результат в list(), чтобы сразу получить обычный список кортежей и можно было его напечатать или сохранить.
  • print(pairs) — выведет [('Alice', 85), ('Bob', 92), ('Carol', 78)].

Список, полученный через zip(), — это уже знакомые кортежи, поэтому его можно сразу сортировать через key и нумеровать через enumerate(), объединяя все три инструмента в одном решении.

Сортировка сразу по двум признакам

Иногда одного признака мало: нужно отсортировать по оценке, а тех, у кого оценка совпала, — ещё и по имени. Заводить для этого несколько отдельных сортировок не нужно.

key может вернуть не одно значение, а кортеж из нескольких. Python сравнивает такие кортежи по очереди: сначала первый элемент, и только если он совпал — смотрит на второй. Это тот же порядок сравнения, что и у слов в словаре: сначала первая буква, при совпадении — вторая.

students = [("Bob", 90), ("Alice", 90), ("Carol", 78)]

ranking = sorted(students, key=lambda s: (-s[1], s[0]))
print(ranking)
  • key=lambda s: (-s[1], s[0]) — для каждого кортежа строим новый кортеж-ключ из двух частей: -s[1] — оценка со знаком минус, s[0] — имя.
  • Минус перед s[1] — способ получить убывание без reverse=True: чем выше оценка, тем меньше её отрицательное значение, а sorted() всегда сравнивает по возрастанию ключа.
  • Второй элемент ключа, s[0] (имя), сработает только тогда, когда у двух студентов совпала оценка — тогда сравнение "провалится" на второй уровень и разберёт их по алфавиту.
  • print(ranking) — выведет [('Alice', 90), ('Bob', 90), ('Carol', 78)]: у Alice и Bob одинаковая оценка 90, поэтому между собой они расставлены по алфавиту, а Carol с меньшей оценкой — в конце.

Частые ошибки

Сохранить результат .sort() в переменную

result = scores.sort() — в result окажется None, потому что .sort() ничего не возвращает, а меняет список scores напрямую. Нужный список — это уже изменённый scores, а не то, что вернул .sort().

Вызвать функцию вместо передачи её в key

key=s[1] вместо key=lambda s: s[1] — выдаст ошибку, потому что key ждёт функцию, которую сам вызовет для каждого элемента, а не готовое значение. Скобки вокруг lambda лучше не забывать.

Перепутать местами номер и значение в enumerate

for name, i in enumerate(names): — enumerate() всегда отдаёт пару (номер, значение) именно в этом порядке, поэтому первой переменной должен идти номер: for i, name in enumerate(names):.

Ждать от zip() пары, если списки разной длины

zip([1, 2, 3], [10, 20]) — вернёт только две пары. zip() останавливается там, где заканчивается самая короткая последовательность, лишние элементы длинной просто отбрасываются без ошибки.

Что важно запомнить

  • sorted(data) возвращает новый список, оригинал не трогает. data.sort() меняет список на месте и возвращает None.
  • key=lambda x: ... говорит, по какой части каждого элемента сравнивать — индекс кортежа, ключ словаря или что угодно ещё.
  • reverse=True разворачивает готовый порядок сортировки на противоположный, ничего не меняя в самом сравнении.
  • Если key возвращает кортеж, Python сравнивает его по элементам по очереди — так получается сортировка сразу по нескольким признакам.
  • enumerate(seq, start=1) в цикле for i, x in ... отдаёт номер и значение вместе, без ручного счётчика.
  • zip(a, b) соединяет два списка по позиции в пары; если списки разной длины, лишние элементы длинного списка отбрасываются.

Проверьте себя

6 вопросов

Лямбда и sorted

Premium

Сортировка по ключу

Дан список кортежей: students = [("Alice", 85), ("Bob", 72), ("Charlie", 90)] — в каждом кортеже имя и оценка.

Напишите функцию sort_by_score(students), которая возвращает список этих же кортежей, отсортированный по оценке (второй элемент кортежа) в порядке убывания. Используйте sorted() с параметрами key и reverse.

Например, для списка выше функция должна вернуть [("Charlie", 90), ("Alice", 85), ("Bob", 72)].

enumerate и zip

Premium