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

公開日:2019-05-17
最終更新:2019-05-17
※この記事は外部サイト(https://idontwannawork.github.io/posts/ite...)からのクロス投稿です

なんのこっちゃ?

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

>>> 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)  
...  
>>>  

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

おわりに

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

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

記事が少しでもいいなと思ったらクラップを送ってみよう!
1
+1
@webmaster909の技術ブログ

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする