はじめに
分け合って... ワケあって... SideM 10thツアー、これはめでたい!!🎊
nikkieです1。
ちょっとトリッキーな見た目のPythonのイディオムzip(*[iter(s)]*n)を完全に理解しました。
目次
- はじめに
- 目次
- 組み込み関数zip()のドキュメント
- zip()でチャンクに分割する
- ぴったりn個ずつに分かれないときには、zip_longest()
- 『Python実践入門』8章で知りました
- 終わりに
組み込み関数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>]
iteratorsをzip()関数に渡したとき、先に見た「イテラブルの左から右への評価順序は保証されています。」という点が効いてきます。
(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()で提供されます。
>>> 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()のドキュメントの箇所が案内されています!
『Python実践入門』8章のコラムにあるzipとiterと*の例を知り、感動に打ち震えていますhttps://t.co/kFqmxq8eM8 のヒントとコツにあります
— nikkie / にっきー 技書博 け-04 Python型ヒント本 (@ftnext) 2023年3月21日
Pythonとアスタリスク 2023入り!
>>> s = range(6)
>>> n = 2 # sからn個ずつ取り出す
>>> list(zip(*[iter(s)]*n, strict=True))
[(0, 1), (2, 3), (4, 5)]
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
- かつてPythonの見た目について話したあの熊本城ホールでも、プロミが...↩
-
あまり直感的ではないかもですね
↩小さい方を読み切ったらforを抜けるの予想と違いますよね。
— nikkie / にっきー 技書博 け-04 Python型ヒント本 (@ftnext) 2022年10月21日
3.10からzipにstrict引数が追加されており、Trueを指定すれば長さが異なるとValueErrorを送出するようになったんです!https://t.co/kFqmxqphO8
また長い方に合わせたいときはzip_longestが標準ライブラリのitertoolsにありますー -
私が
zip_longest()を初めて知ったのは、『Effective Python 第2版』項目8でしたね ↩