おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

リストをn個ずつのサブリストに分割 (Python)

愚直にやればもちろんできるんだけど、パフォーマンスが気になったり、かっこよく書きたいとか思うわけです。

>>> a = range(15)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
>>> [a[i:i+3] for i in range(0,15,3)]
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], [12, 13, 14]]

range(0, 15, 3) が [0, 3, 6, 9, 12] に展開されて、スライスしていく感じ。

zip(*[iter(s)]*n) で n 個ずつ分割

探しているとこんなのがでてくる。

iterable の、左から右への評価順序が保証されます。そのため zip(*[iter(s)]*n) を使ってデータ系列を n 長のグループにするクラスタリングすることができます。

ということで、15個の要素を持つリストを3個ずつのサブリストに分割したい場合、

>>> zip(*[iter(range(15))]*3)
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), (12, 13, 14)]

となるのだそうだ。なんで?

zip関数

そもそもzip関数は何をするかというと、たとえば、以下の例。

>>> zip(['a','b','c'], ['A','B','C'], ['1','2','3'])
[('a', 'A', '1'), ('b', 'B', '2'), ('c', 'C', '3')]

引数に渡したイテレータを順番になめて、要素をとってきて、tupleにする。 keyとvalueのリストから辞書を作るときにつかうやつです。

>>> key = ['a', 'b', 'c']
>>> val = ['A', 'B', 'C']
>>> dict(zip(key,val))
{'a': 'A', 'c': 'C', 'b': 'B'}

引数が同じイテレータだったら

ということを踏まえて、先ほどのコード。同じイテレータが引数に渡されている。

>>> zip(*[iter(range(15))]*3)
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), (12, 13, 14)]

展開するとこう。

>> x = iter(range(15))
>>> zip(x, x, x)
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), (12, 13, 14)]

なるほど!

このあたりに解説があった。

残念な点

最高にクールでかっこいいのだが、余りがある場合は捨てられる。

>>> zip(*[iter(range(16))]*3)
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), (12, 13, 14)]

itertools.izip_longest

ちゃんと余った要素もリストにしてほしい。ということで、そのときは zip の代わりに itertools.izip_longest を使えばいいようだ。

>>> for a in itertools.izip_longest(*[iter(range(16))]*3):
...     print a
... 
(0, 1, 2)
(3, 4, 5)
(6, 7, 8)
(9, 10, 11)
(12, 13, 14)
(15, None, None)

ふむ。

2013/07/11 追記

いろいろブコメありがとうございました!

id:kk6 more_itertools.chunked も同じような実装

恥ずかしながらmore_itertoolsを知りませんでした。itertoolsも忘却の彼方でしたし。。

欲しかったのはズバリこれでした。

id:tomoemon パッと見てイミフなコードは3ヶ月後に見たらまたイミフになるだけ。そういう "beautiful code" が本当に必要な場面かどうかよく吟味して使いたい

はい、そのとおりです。 トリッキーなコードをむやみに書くべきではないのですが、パフォーマンス的に、直感的でなかったり、意図を汲み取りにくいコードが必要になる場合はあると思います。(スクリプト言語は静的言語ほどの最適化が走らないので余計に)

とはいえ、こんなコード片をそのままリリースしてたら、レビューで意味わからんと怒られてしまいます。ということで、

  • これは何をしてるんだということをコメントで書いておく
    • コードレビューではより細かく補足することも
  • 場合によっては、メソッドを分離してテストを書く
    • メソッドとして独立させることで、メソッド名と引数、ドキュメントで意味がわかりやすくなる
    • テストを書くことでさらに挙動を追いやすくなる。というかこの手のトリッキーなコードは特異条件だけで成立するようなことも多く (上でいうと、割り切れないと切り落とされるとか)、そこを補完するのにも役立つと思います。

というのは意識しています。