概要
- やりたかったこと
- pythonでシーケンスからn件ずつ取り出してループしたい。
- 末尾にn件未満の要素が残った場合、それも取り出したい。
- 結論
- ループの中でスライスを取る。
- Iteratorとzipを使ったら、n件未満の要素が取り出せなかった(仕様通り)。
- itertools.zip_longestを使うと、末尾に余分な要素が追加されてしまった(これも仕様通り)。
- 環境
- python3.6.7
本題
シーケンスから要素を何件かまとめて取り出したいケースって、たまにあると思います。100件のシーケンスから3件ずつ取り出す・・・というような感じです。
100件から3件ずつだと、最後は1件だけになります。この最後の1件を、1件しかないことを気にせずに取り出したい、というのがこの記事の目的です。
愚直にスライスを取る
この記事で意図通りに動くのはここだけです。
私はまだpythonを勉強がてらに始めたばかりなので、素直に考えつくのはこんな方法でした。
test_data = ['いちばん', 'にばん', 'さんばん', 'よんばん', 'ごばん']
# 最大で2つずつ取り出したい
size = 2
for start in range(0, len(test_data), size):
print(test_data[start : start + size])
その出力
['いちばん', 'にばん']
['さんばん', 'よんばん']
['ごばん']
シンプルです。スライスを取る時に、シーケンスの範囲外を指定されてもエラーを起こさないという、pythonの優しさのおかげで、ループの最終3回目はtest_data[4:6]
ですが問題なく取り出せます。
しかし、もしかしたら、もっと楽な、スマートな方法がないだろうか?と思って探してみました。
・・・探してみたけどうまくいかなかったので、試行錯誤は折り畳みます。
zipを使った場合(失敗しました)
test_data = ['いちばん', 'にばん', 'さんばん', 'よんばん', 'ごばん']
size = 2 # 最大で2つずつ取り出したい
iterator = iter(test_data)
for parted in zip(*[iterator]*size):
print(parted)
上のコードの出力内容
('いちばん', 'にばん')
('さんばん', 'よんばん')
最後の要素が省かれてしまいます。
元々からしてzipは長さの違う複数のイテラブルを渡すな。という仕様なので、これはごもっともな動きです。
zip_longestを使った場合(これも失敗しました)
test_data = ['いちばん', 'にばん', 'さんばん', 'よんばん', 'ごばん']
size = 2 # 最大で2つずつ取り出したい
iterator = iter(test_data)
for parted in itertools.zip_longest(*[iterator]*size):
print(parted)
上のコードの出力内容
('いちばん', 'にばん')
('さんばん', 'よんばん')
('ごばん', None)
最後にNoneが付いてきました。ケースバイケースで、これでいい場合もあると思いますが、今回は残念ながら違いました。
とはいえ、詰め物をしてくるのは使う前に知っていたので、これは仕方ない。
結論
素直にスライス取っちゃおうよ。ということになりました。
zipを使う方法は、**シーケンスの長さが一度に取り出したい数の倍数ぴったりなら、うまく行きます。**それでいいというケースも多々あるはずで、目的が違うなら仕方ないですね。
しかし、わざわざスライスを取る1行を省略できないか?1と思って探してみましたが、私が見つけられなかっただけかも知れないので、もしあったら知りたいところです。
-
いっそのことイテラブルなクラスを書いてしまおうかと一瞬考えましたが、それじゃ本末転倒ですね。よほど同じ処理を繰り返すのでなければコードが増えるだけです。そして、そこまでこの処理を繰り返すコードは、ちょっと考えたくないですw ↩