Iteratorの中で要素を削除するということ

Javaの場合

import java.util.ArrayList;
import java.util.List;

class Hoge {
    public static void main(String []args) {
        List<Integer> samples = new ArrayList<Integer>();
        samples.add(0);
        samples.add(1);
        samples.add(2);
        samples.add(3);
        samples.add(4);
        samples.add(5);
        samples.add(6);
        samples.add(7);
        samples.add(8);
        samples.add(9);

        System.out.println(samples);

        for (Integer i : samples) {
            if (i % 3 == 0) {
                samples.remove(i);
            }
        }
        
        System.out.println(samples);
    }
}

というコードを書いて、実行してみると、

$ javac Hoge.java
$ java Hoge
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:449)
	at java.util.AbstractList$Itr.next(AbstractList.java:420)
	at Hoge.main(Hoge.java:20)

となる。

import java.util.ArrayList;
import java.util.List;

class Hoge {
    public static void main(String []args) {
        List<Integer> samples = new ArrayList<Integer>();
        samples.add(0);
        samples.add(1);
        samples.add(2);
        samples.add(3);
        samples.add(4);
        samples.add(5);
        samples.add(6);
        samples.add(7);
        samples.add(8);
        samples.add(9);
        samples.add(0);

        System.out.println(samples);

        java.util.Iterator<Integer> iter = samples.iterator();
        while (iter.hasNext()) {
            if (iter.next() % 3 == 0) {
                iter.remove();
            }
        }

        System.out.println(samples);
    }
}

と書くと、

$ javac Hoge.java
$ java Hoge
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
[1, 2, 4, 5, 7, 8]

と、正しく3の倍数を除いたものが得られる。


ConcurrentModificationExceptionというのは、繰り返し処理の途中でCollectionが変更されたときにスローされるらしい。
Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle
1番目のコードはモロにそういうことをしているので、見事にスローされてしまい失敗しているようだ。
2番目のコードは、そのIteratorが行う処理としてremoveをしているので、それがサポートされていればIteratorの動作を保証したまま要素の削除を行うことができる、ということらしい。
Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle


必ずしもremoveがサポートされているわけでもないようで、例えば以下のようなコードを書くと

import java.util.Arrays;
import java.util.List;

class Hoge {
    public static void main(String []args) {
        List<Integer> samples = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
        System.out.println(samples);

        java.util.Iterator<Integer> iter = samples.iterator();
        while (iter.hasNext()) {
            if (iter.next() % 3 == 0) {
                iter.remove();
            }
        }

        System.out.println(samples);
    }
}

こういう結果になる。

$ javac Hoge.java 
$ java Hoge
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.AbstractList.remove(AbstractList.java:172)
	at java.util.AbstractList$Itr.remove(AbstractList.java:437)
	at java.util.AbstractCollection.remove(AbstractCollection.java:255)
	at Hoge.main(Hoge.java:12)

Pythonの場合

Javaのように例外がスローされることもなく、普通にfor文の繰り返しの中で要素の削除をすることができてしまう。

#!/usr/bin/env python
samples = [1, 2, 3, 4, 5, 6, 7, 8, 9]
for elem in samples:
    if elem % 3 == 0:
        samples.remove(elem)

print samples
$ python hoge.py
[1, 2, 4, 5, 7, 8]

実行結果も正しいものが得られるようにみえる。


が、以下のように変更してみると、結果が期待に反することが分かる。

#!/usr/bin/env python
samples = [0, 1, 2, 3, 3, 4, 5, 6, 6, 6, 7, 8, 9, 9, 9, 9]
for elem in samples:
    if elem % 3 == 0:
        samples.remove(elem)

print samples
$ python hoge.py
[1, 2, 3, 4, 5, 6, 7, 8, 9, 9]

3の倍数が幾つか残っている。
おそらく、要素が削除されたときに詰められて、Iteratorが見ている先が変化してしまうから?

 |
[0, 1, 2, 3, 3, 4, 5, 6, 6, 6, 7, 8, 9, 9, 9, 9]
    |
[0, 1, 2, 3, 3, 4, 5, 6, 6, 6, 7, 8, 9, 9, 9, 9]
       |
[0, 1, 2, 3, 3, 4, 5, 6, 6, 6, 7, 8, 9, 9, 9, 9]
          |
[0, 1, 2, 3, 3, 4, 5, 6, 6, 6, 7, 8, 9, 9, 9, 9]
(ここで"3"の要素が削除される)
             |
[0, 1, 2, 3, 4, 5, 6, 6, 6, 7, 8, 9, 9, 9, 9]
                |
[0, 1, 2, 3, 4, 5, 6, 6, 6, 7, 8, 9, 9, 9, 9]
...


ということで、普通に書けるし実行できてしまうけど、このように「繰り返し処理の中で要素を削除する」ということはすべきではないようだ。


pythonにはfilterというビルトイン関数があるので、フィルタリングしたいときはそれを使って新しいリストを生成することができる。

#!/usr/bin/env python
samples = [0, 1, 2, 3, 3, 4, 5, 6, 6, 6, 7, 8, 9, 9, 9, 9]

print filter(lambda x: False if x % 3 == 0 else True, samples)
$ python hoge.py
[1, 2, 4, 5, 7, 8]