← все задачи

Python · задача 4 из 10

Дубликаты и нарезка на пачки

Начальный 10–15 минут множествагенераторыitertools

Условие

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

Что требуется

  • Дедупликация — за один проход
  • Нарезка работает с любым итерируемым, включая генератор и файл
  • Последняя пачка может быть неполной

Пример

dedup([3, 1, 3, 2, 1])
# [3, 1, 2]

list(chunked([1, 2, 3, 4, 5], 2))
# [[1, 2], [3, 4], [5]]

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Элементы хешируемые? Если это словари — множество не подойдёт
  • Нужен список на выходе или достаточно генератора?
  • Что делать при size <= 0 — падать или вернуть пусто?
Показать решение Скрыть решение

Решение

from itertools import islice


def dedup(items):
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result


def chunked(iterable, size):
    if size <= 0:
        raise ValueError("size must be positive")

    iterator = iter(iterable)
    while True:
        batch = list(islice(iterator, size))
        if not batch:
            return
        yield batch


# Python 3.12+: то же самое есть в стандартной библиотеке
# from itertools import batched

Почему так

Почему set, а не «item not in result»

  • Проверка вхождения в список — O(n), и весь цикл превращается в O(n²): на 100 000 элементов это уже минуты
  • Множество даёт O(1) на проверку ценой памяти — классический размен, который здесь и проверяют
  • list(dict.fromkeys(items)) — однострочник с тем же эффектом: порядок сохраняется с Python 3.7

Почему islice, а не срезы по индексам

  • Срез items[i:i + size] требует список — генератор или файл так не нарезать, а именно ими обычно и приходят данные
  • islice тянет ровно size элементов из любого итератора и не читает лишнего
  • Решение через yield не держит все пачки в памяти: обработали батч — забыли

Почему iter(iterable) в начале

  • islice от самого списка каждый раз начинала бы сначала — получился бы бесконечный цикл из одной и той же пачки
  • Явный iter() фиксирует позицию чтения между итерациями
  • Это любимая ловушка в этой задаче: если написать без iter, всё «работает», пока не передадут список

Что спросят дальше

  • Нехешируемые элементы: для словарей ключом делают кортеж нужных полей или json.dumps(sort_keys=True)
  • Спросят про дедупликацию по полю: seen.add(item["id"]) — и функция становится dedup_by(items, key)
  • Спросят, знаете ли itertools.batched из 3.12 и что делать на старых версиях

Следующая задача

Слить пересекающиеся интервалы — Задача про бронирования и расписания: единственный алгоритмический сюжет, который правда встречается в бэкенде.