nikkie-ftnextの日記

イベントレポートや読書メモを発信

Pythonの組み込み関数zipとiterで、イテラブルを要素n個ずつのチャンクに分割できる

はじめに

分け合って... ワケあって... SideM 10thツアー、これはめでたい!!🎊
nikkieです1

ちょっとトリッキーな見た目のPythonのイディオムzip(*[iter(s)]*n)を完全に理解しました。

目次

組み込み関数zip()のドキュメント

https://docs.python.org/ja/3/library/functions.html#zip

複数のイテラブルを並行に反復処理し、各イテラブルの要素からなるタプルを生成します。

イテラブルとは、シーケンス(リスト、文字列、タプル)、辞書、ファイルオブジェクト。
また、自分で作ることもできます(__iter__()または__getitem__()を実装したクラス)。
https://docs.python.org/ja/3/glossary.html#term-iterable

ヒントとコツより

イテラブルの左から右への評価順序は保証されています。
そのため zip(*[iter(s)]*n, strict=True) を使ってデータ系列を長さ n のグループにクラスタリングするイディオムが使えます。
(略)
これは入力を長さ n のチャンクに分割する効果があります。

zip()でチャンクに分割する

6要素のリストを2要素ずつのチャンクに分割してみましょう。
この記事ではPython 3.12.3で動作確認しています。

>>> iterable = range(6)
>>> iterators = [iter(iterable)] * 2
>>> list(zip(*iterators))
[(0, 1), (2, 3), (4, 5)]

iterableイテレータにし、同一のイテレータを持ったリストiteratorsを作っています。
idが同じ、すなわち、メモリ上の位置が同じですね。

>>> iterators
[<range_iterator object at 0x10283f840>, <range_iterator object at 0x10283f840>]

iteratorszip()関数に渡したとき、先に見た「イテラブルの左から右への評価順序は保証されています。」という点が効いてきます。
zip(*iterators)は、zip()にn個のイテレータを渡しています。詳しくは過去の発表資料をどうぞ)

  • zip()iterators[0]側(左側)を1要素取り出す -> 0
  • 続いてzip()iterators[1]側(右側)から1要素取り出す
    • このとき、iterators[0]iterators[1]指すイテレータは同一であり、(先ほど0を返したので)次に返す要素は1
  • ここまででzip()(0, 1)というタプルを返します
  • イテレータが尽きるまでこれを続けます
    • zip()iterators[0]側(左側)を1要素取り出すと、イテレータは1まで来たので、次は2を返します

ぴったりn個ずつに分かれないときには、zip_longest()

7要素のイテラブルを3要素ずつに分けてみましょう。

>>> iterable = "ABCDEFG"
>>> iterators = [iter(iterable)] * 3
>>> list(zip(*iterators))
[('A', 'B', 'C'), ('D', 'E', 'F')]

おや、Gが抜けてしまいました。
その理由はzip()のドキュメントにあります2

デフォルトでは、 zip() は最も短いイテラブルが消費しきった時点で停止します。より繰り返し数の長いイテラブルの残りの要素は無視して、結果を最も短いイテラブルの長さに切り詰めます

代替案もzip()のドキュメントにあります3

短いイテラブルを一定の値でパディングして全てのイテラブルが同じ長さになるようにすることもできます。この機能は itertools.zip_longest() で提供されます。

https://docs.python.org/ja/3/library/itertools.html#itertools.zip_longest

>>> from itertools import zip_longest
>>> iterable = "ABCDEFG"
>>> iterators = [iter(iterable)] * 3
>>> list(zip_longest(*iterators))
[('A', 'B', 'C'), ('D', 'E', 'F'), ('G', None, None)]

最後の要素に('G', None, None)があります!
Noneが登場したのはzip_longest()fillvalue引数のデフォルト値がNoneだからです。

ABC / DEF / G とチャンク分けするサンプルプログラムです。

>>> iterable = "ABCDEFG"
>>> iterators = [iter(iterable)] * 3
>>> for chunk in zip_longest(*iterators, fillvalue=""):
...     print("".join(chunk))
...
ABC
DEF
G

Python実践入門』8章で知りました

8.2のコラムに「zip()iter()を使ったイディオム」があり、今回取り上げたzip()のドキュメントの箇所が案内されています!

1年以上前に出会ったときと比べて、同じイテレータ(メモリ上の同じ位置のデータ)を参照しているからチャンクに分けられることが今回はより理解できた感覚です

終わりに

組み込み関数zip()iter()で、イテラブルをチャンクに分けられます!

  • n要素のチャンクに分けたいとき、zip(*[iter(iterable)]*n)でできる
  • チャンク分けで端数が出ることも考慮してitertools.zip_longest(*[iter(iterable)]*n)がいいのでは

itertoolsのドキュメントのレシピにあるgrouper()関数の実装を見ていて、zip()iter()でチャンク分けを思い出しました。
https://docs.python.org/ja/3/library/itertools.html#itertools-recipes


  1. かつてPythonの見た目について話したあの熊本城ホールでも、プロミが...
  2. あまり直感的ではないかもですね
  3. 私がzip_longest()を初めて知ったのは、『Effective Python 第2版』項目8でしたね