Чтобы удалить дубликаты и сохранить порядок, мало собрать уникальные значения. Программа должна пройти исходную последовательность слева направо, запомнить уже встреченные элементы и оставить только первое появление каждого из них.
Простой set(items) отвечает на вопрос «какие значения встретились», но не хранит позиции и порядок вставки. Поэтому множество удобно для проверки повторов, а порядок результата должна задавать отдельная последовательность. Примеры ниже проверены на CPython 3.14.5.
Что значит «сохранить порядок»
Для списка ['open', 'click', 'open', 'pay', 'click'] результат должен содержать open, click и pay именно в такой последовательности. Сортировка не подходит: она переставит значения по алфавиту, а не по моменту первого появления.
| Позиция | Значение | Уже встречалось | Действие |
|---|---|---|---|
| 0 | open | нет | оставить |
| 1 | click | нет | оставить |
| 2 | open | да | пропустить |
| 3 | pay | нет | оставить |
| 4 | click | да | пропустить |
Такое правило называют стабильной дедупликацией: относительный порядок оставшихся элементов совпадает с исходным.
Почему одного 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 для начинающих». В нём эта задача продолжает практику коллекций без скрытой сортировки и изменения входных данных.