Разбираемся в работе функции grouper в Python
Часто на StackOverflow задают вопрос «Как разбить последовательность на части равного размера?». Давайте попробуем в нём разобраться.
5К открытий5К показов
Часто на 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
и понимать, как он работает:
5К открытий5К показов