Как удалить дубликаты из списка Python и сохранить порядок

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

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

Простой set(items) отвечает на вопрос «какие значения встретились», но не хранит позиции и порядок вставки. Поэтому множество удобно для проверки повторов, а порядок результата должна задавать отдельная последовательность. Примеры ниже проверены на CPython 3.14.5.

Что значит «сохранить порядок»

Для списка ['open', 'click', 'open', 'pay', 'click'] результат должен содержать open, click и pay именно в такой последовательности. Сортировка не подходит: она переставит значения по алфавиту, а не по моменту первого появления.

ПозицияЗначениеУже встречалосьДействие
0openнетоставить
1clickнетоставить
2openдапропустить
3payнетоставить
4clickдапропустить

Такое правило называют стабильной дедупликацией: относительный порядок оставшихся элементов совпадает с исходным.

Почему одного set недостаточно

Множество хранит только уникальные хешируемые элементы. Официальная документация прямо называет set неупорядоченной коллекцией: у элементов нет позиции и индекса.

events = ["open", "click", "open", "pay", "click"]
unique = set(events)

print(len(unique))
print(unique == {"open", "click", "pay"})
3
True

Проверки выше детерминированы: они не зависят от того, в каком порядке интерпретатор напечатает множество. Полагаться на вид set в консоли нельзя.

Сортировка делает вывод предсказуемым, но меняет смысл:

events = ["view", "buy", "click", "view"]
print(sorted(set(events)))
['buy', 'click', 'view']

Первым во входе был view, а после сортировки он стал последним. Для журнала событий, очереди или истории действий это уже другие данные.

Как работает проверка «уже встречалось»

Состояние хранится в множестве seen. Перед добавлением очередного значения проверяют оператором in, было ли оно раньше:

seen = {"page_open"}

for event_id in ["page_open", "cta_click"]:
    print(event_id, event_id in seen)
    seen.add(event_id)
page_open True
cta_click False

Проверка выполняется до add: после добавления оба значения уже считались бы встреченными. В полном алгоритме первое появление дополнительно попадает в новый список результата. Исходный список при этом менять не нужно.

Так разделяются две роли:

  • set быстро отвечает, встречалось ли значение;
  • list хранит элементы в порядке первых появлений.

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

Когда подходит словарь

Начиная с Python 3.7 порядок вставки ключей в dict гарантирован спецификацией языка. Повторное присваивание существующему ключу не перемещает его в конец:

positions = {"open": 0, "click": 1}
positions["open"] = 2
positions["pay"] = 3

print(list(positions))
['open', 'click', 'pay']

На этом свойстве основан короткий приём с dict.fromkeys. Он подходит, когда элементы хешируемы и нужно оставить именно первое равное значение. Явный обход с seen длиннее, зато в нём проще добавить нормализацию, журналирование или собственное правило сравнения.

Ограничения set и dict

Строки, числа и кортежи из хешируемых элементов можно положить в множество. Список изменяем, поэтому собственного стабильного хеша у него нет:

seen = set()

try:
    seen.add(["open", "click"])
except TypeError as error:
    print(type(error).__name__)
TypeError

Если вход содержит списки или словари, сначала определите ключ сравнения. Например, для записи события им может быть строковый event_id, а не весь изменяемый объект. Универсально превращать всё в строку опасно: разные данные способны получить одинаковое текстовое представление.

Подробнее о том, почему list изменяем, а tuple может быть хешируемым, читайте в разборе изменяемых и неизменяемых типов.

Равенство тоже влияет на дубликаты

Множества и ключи словарей учитывают равенство и хеш. В Python значения 1, 1.0 и True равны между собой, поэтому схлопываются в один элемент. Строки с разным регистром, напротив, не равны:

values = {1, 1.0, True}

print(len(values))
print("a" == "A")
1
False

Для списка идентификаторов это означает: не приводите строки к нижнему регистру, если бизнес-правило не требует считать A и a одним значением. Нормализация меняет условие равенства, а значит, и результат дедупликации.

Пограничные случаи и частые ошибки

Перед реализацией проверьте четыре сценария:

  • пустой вход должен дать новый пустой список;
  • список из одного элемента остаётся таким же по значению;
  • повтор первого элемента в конце не меняет его позицию;
  • входной список после вызова остаётся неизменным.

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

ОшибкаЧто ломается
list(set(items))порядок результата не задан
sorted(set(items))порядок заменяется сортировкой
добавление в seen до проверкикаждый элемент выглядит повтором
удаление из исходного списка в циклеиндексы сдвигаются, вход меняется
безусловный .lower()разные идентификаторы объединяются
элементы-списки внутри setвозникает TypeError

Как закрепить механику

В задаче «Дедупликация событий» нужно вернуть новый список идентификаторов и сохранить порядок их первых появлений. Начните с пустого seen, отдельно заведите результат и проверьте границы из таблицы ошибок.

Если основные операции со списками и множествами пока требуют подсказки, пройдите путь «Python для начинающих». В нём эта задача продолжает практику коллекций без скрытой сортировки и изменения входных данных.

Источники