頑張らないために頑張る

ゆるく頑張ります

イテレータを複数回ループしたい

Posted at — May 15, 2019

なんのこっちゃ?

実行しようとしていたのはこんなコードでした。

>>> import re
>>> s = "hogefugapiyofoobarbaz1234567890abc987efg654hij321"
>>> iter = re.finditer("b..", s)    ← finditer()は結果をイテレータで返す
>>> for i in iter:
...     print(i.start())
...
15
18
32
>>> for i in iter:
...     print(i.start())
...
>>>    ← 同じループを実行しても最初のループと異なり結果が返ってこない

このように、同一のイテレータに対しループ処理を複数回行うと、2回目以降のループは結果が空になってしまいます。

ちなみにジェネレータでも上記のような複数回のループ処理を行おうとすると、2回目以降のループで結果が空になるらしいですが、ジェネレータについては別途まとめて記事にしようと思います(まだ勉強中)。

なんでこーなるの?

イテレータが持つ要素を取得したい場合、__next__() メソッド(または組み込み関数のnext())を繰り返し呼び出すと、イテレータ中の要素を1つずつ返します。このメソッドは集合から1つずつ要素を取り出しています。取り出しているので、すべて取り出し終わったら元の集合には要素が存在しません。よって2回目以降のループは空っぽになります(要素がない場合は、StopIteration例外を返す)。

※「取り出す」という表現が正確かどうかはちょっと自信がありません。メソッドや関数の「next」という名前の通り「次の要素へ」という挙動と、同じ要素を複数回取得できないことから「取り出す」という表現を使っています。

なお、直接関係はありませんが、map()filter()はイテレータを返す(Python3での話)ので、返されたオブジェクトについてlist()などを複数回実行すると、上記のように2回目以降は空っぽになってしまうようです。

>>> list = [1, 2, 3]
>>> f = filter(None, list)
>>> list(list)
[1, 2, 3]
>>> list(list)
[1, 2, 3] ← リストlistに複数回listしても結果が返ってくる
>>> list(f)
[1, 2, 3]
>>> list(f)
[] ← イテレータに複数回listすると2回目以降ブランクになる
>>>

そもそもイテレータって?

iteratorとはオブジェクトの一種で、データの走査方法について表現するものです。なんのこっちゃ、という感じですが「要素を1つずつ繰り返し取得できる構造を持っていて(iterable)、実際に順次取得ができる」オブジェクトっていう感じかと。

>>> list = [1, 2, 3]
>>> i = iter(list)
>>> type(list)
<class 'list'>
>>> type(i)
<class 'list_iterator'>
>>> print(next(i))
1
>>> print(next(i))
2
>>> print(next(i))
3
>>> print(next(i))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration ← 「これ以上next()で取り出せる要素ねぇよ!」と言っている
>>>

これと似たオブジェクトにitetable(反復可能オブジェクト)があります。こちらはデータの構造そのものについて表現しており、iteratorとは別物です。たとえばリストやタプル、辞書などはiterableで、オブジェクトに対しアクセスすることで、要素を1つずつ取得することができる構造のことを指しています。つまり、先述のiteratoriterableに含まれるわけです。

ちなみに「iterable」の英単語本来の意味は「繰り返し可能な」という形容詞

どうすれば回避できる?

変数に格納する

再利用したいなら、単純に変数へ格納しちゃえという方法。

>>> import re
>>> s = "hogefugapiyofoobarbaz1234567890abc987efg654hij321"
>>> iter = re.finditer("b..", s)
>>> type(iter)
<class 'callable_iterator'>
>>> lists = list(iter)
>>> type(lists)
<class 'list'>
>>> for i in lists:
...     print(i)
...
<_sre.SRE_Match object; span=(15, 18), match='bar'>
<_sre.SRE_Match object; span=(18, 21), match='baz'>
<_sre.SRE_Match object; span=(32, 35), match='bc9'>
>>> for i in lists:
...     print(i.start())
...
15
18
32
>>> for i in lists:
...     print(i.start())
...
15
18
32    ← 複数回ループしても結果が返ってきている
>>>

filter()などのイテレータを返すものも同様。

>>> list = [1, 2, 3]
>>> f = filter(None, list)
>>> type(f)
<class 'filter'>
>>> listed = list(f)
>>> type(listed)
<class 'list'>
>>> list(listed)
[1, 2, 3]
>>> list(listed)
[1, 2, 3]
>>> list(f)
[]
>>>

リストでループする

今回の場合で言うとfinditer()ではなくfindall()を用いて、イテレータでなくリストでループするようにします。

>>> list = re.findall("b..", s)
>>> for item in list:
...     print(item)
...
bar
baz
bc9
>>> for item in list:
...     print(item)
...
bar
baz
bc9

findall()はリストを返すメソッド。リストlistに対しては、ループ処理を何回行っても同様な結果が出力されます。これなら上記のような問題は発生しませんが、このあたりは要求される機能と相談する必要があると思います。

itertoolsを用いる

再利用する回数が事前に分かっているならitertools.tee()を利用する方法もあります。

>>> import itertools
>>> list = [1, 2, 3]
>>> i = iter(list)
>>> i1, i2, i3 = itertools.tee(i, 3) ← 3回再利用する必要があると仮定
>>> for n in i1:
...     print(n)
...
1
2
3
>>> for n in i1:
...     print(n)
...
>>> for n in i2:
...     print(n)
...
1
2
3
>>> for n in i2:
...     print(n)
...
>>> for n in i3:
...     print(n)
...
1
2
3
>>> for n in i3:
...     print(n)
...
>>>

ただ、個人的にはこの方法を利用するようなシチュエーションがあまり思い浮かばない・・・。

おわりに

「ん?何で同じ条件なのにループすると空っぽになるんじゃ?」と素朴に思ったのが始まりなのですが、調べてみると案外深い仕様になっていて勉強になりました(小並感)。

ちなみに、複数回ループしようとしてた理由は、原因を調べているうちに忘れました(鳥頭)。

comments powered by Disqus