Разбираемся в работе функции grouper в Python
Часто на StackOverflow задают вопрос «Как разбить последовательность на части равного размера?». Давайте попробуем в нём разобраться.
6К открытий7К показов
Часто на StackOverflow задают вопрос «Как разбить последовательность на части равного размера?». Давайте попробуем в нём разобраться.
Если это действительно последовательность, а не произвольный итерируемый объект, то разбить её можно с помощью срезов. Однако в документации itertools есть отличный способ сделать это с помощью функции grouper.
Несмотря на то, что пример очень короткий и простой, не все понимают, как он работает. А разобраться стоит, ведь если понять устройство grouper, то будет проще вникнуть в более сложное программирование, основанное на итераторах.
Пары объектов
Начнём с более простой функции для группировки итератора по объектам чётной длины в итератор по парам объектов:
Как это работает?
Сначала мы создаём итератор по итерируемому объекту. Итератор — объект, который может выдавать элементы по одному при вызове функции next(). Итератор сам по себе является итерируемым объектом: при вызове iter() он возвращает сам себя. Чаще всего мы встречаем итераторы в виде объектов, которые возвращаются генераторами, функциями map, filter, zip, функциями в itertools и т.д. Однако мы можем создать итератор для любого итерируемого объекта с помощью функции iter. Например:
Поскольку мы прошлись по i во время первого вызова, во время второго там уже ничего нет. Для лучшего понимания давайте взглянем на функции вроде islice или takewhile, которые поглощают только часть итератора:
Возможно вы задаётесь вопросом, что бы случилось, будь a итератором изначально. В этом случае iter просто возвращает a.
Если у нас есть две ссылки на один и тот же итератор, и мы используем одну из них, то и вторая тоже используется. Создание двух отдельных итераторов для одного и того же итерируемого объекта позволяет этого избежать. Например:
Само собой, если a уже являлся итератором, вызов iter(a) вернул бы нам сам а дважды, и результаты выполнения первого и второго примеров были бы одинаковы.
А что произойдет, если применить функцию zip к двум ссылкам на один итератор? Каждая получит через одно значение:
Если вам сложно понять, почему так происходит, посмотрите, как работает этот упрощенный zip на чистом Python:
Если i1 и i2 — один и тот же итератор, после строки v1 = next(i1) i1 и i2 будут указывать на следующее значение после v1, поэтому v2 = next(i2) получит это значение.
Прим. перев. Этот бесконечный цикл завершится с исключением StopIteration, когда один из итераторов закончится. Но начиная с версии Python 3.7, выброшенное исключение StopIteration внутри генератора не приведёт к его нормальному завершению, а будет перевыброшено снаружи как RuntimeError. Таким образом, для поддержки новых версий Python этот код нужно модифицировать. Подробнее можно почитать на официальном сайте Python.
Вот и всё, что нужно знать о функции pairs.
Части произвольного размера
А как же сделать n ссылок на один и тот же итератор? Для этого есть несколько способов, но вот самый простой:
args = [iter(iterable)] * n
Теперь нужно придумать, как использовать zip в этом случае. Так как zip принимает любое количество аргументов, а не ограничивается двумя, мы можем использовать распаковку списка аргументов:
zip(*args)
И теперь мы почти можем написать grouper:
Части разного размера
И наконец, что будет, если количество элементов нельзя поделить на части равного размера? Например, что произойдет, если вы захотите получить группы по 3 элемента для range(10)? Вот несколько возможных вариантов:
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)][(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 0, 0)][(0, 1, 2), (3, 4, 5), (6, 7, 8), (9,)][(0, 1, 2), (3, 4, 5), (6, 7, 8)]- Будет выброшено исключение
ValueError
Используя zip, мы получаем четвёртый результат: неполная группа не появляется вовсе. Иногда это именно то, что вам нужно. Но зачастую вы скорее всего захотите получить один из первых двух результатов.
Для достижения этой цели в библиотеке itertools есть функция zip_longest (izip_longest в Python 2.x). Вместо отсутствующих значений она вставляет None или аргумент fillvalue. Например:
И вот у нас есть всё, что нужно, чтобы написать grouper и понимать, как он работает:
6К открытий7К показов



