# 6.2. 例外か関数か

例外の方が速い

# 前置き

Python のイテレータとは for 文そのものです。 イテレータを理解すると for 文を、より深く理解できます。 イテレータについては下記の記事で解説させていただきました。

# Python のイテレータ

Python では、ベタ書きして StopIteration という例外を投げて イテレータが終了したかどうかを判定します。

# Python は、Python ... 例外 StopIteration
# 対話モード >>> にコピペで実行できます。
def main():
    container = range(10)
    iterator = Reverse(container)
    for element in iterator:
        print(e)

class Reverse:
    def __init__(self, seq):
        self._seq = seq
        self._index = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        self._index -= 1
        try:
            return self._seq[self._index]
        except IndexError:
            raise StopIteration

if __name__ == '__main__':
    main()

# Java のイテレータ

それに対して Java のイテレータパターンは、 専用の関数 hasNext を実装してイテレータが終了したかどうかを判定します。

Java の interface, implements の話は 「継承より合成ってなに?」 で、 すこしだけ書かせていただきました。正直、自分もあまり理解はしていません。 こんなんなんだなーくらいに流していただけると幸いです。

paiza.ip にコピペで実行できます。

import java.util.Iterator;

// Java は基本的にメソッドは
// 全てクラスに属していないといけないので
// Main クラスにとりあえず main 関数を登録しました。
public class Main {
    public static void main(String [] args) {
        Integer [] container = {0, 1, 2, 3, 4, 5, 6 ,7, 8, 9};
        Reverse<Integer> iterator = new Reverse<>(container);
        for (Integer element : iterator) {
            System.out.println(element);
        }
    }
}

class Reverse<T> implements  Iterable<T>, Iterator<T> {
    private int index;
  	private T [] container;
    public Reverse(T [] container) {
      this.container = container;
      this.index = container.length;
    }
  
    // Iterable の抽象メソッド
    @Override
    public Iterator<T> iterator(){
      return this;
    }
    
    // Iterator の抽象メソッド
  	@Override
    public boolean hasNext(){
      return this.index > 0;
    }
    
    // Iterator の抽象メソッド
  	@Override
    public T next(){
      this.index -= 1;
      return this.container[this.index];
    }
}

公式ドキュメントでインターフェイスが定められていました。

# なんで Python は、例外なの?

なぜ、Python では、このように実装しているのでしょうか?答えは、関数呼び出しが重いからです。

解決した問題 - PEP 234 Iterators
Resolved Issues - PEP 234 Iterators

イテレーションの終端であることを示すシグナルに対して、 例外を使うことがあまりにも処理が重すぎないか、疑問であった。
It has been questioned whether an exception to signal the end of the iteration isn't too expensive.

end 関数を呼び出すことは、1度のイテレーションで2度の呼び出しをしないといけない。 2つの呼び出しは、例外の検査を含めた1度の呼び出しよりも重い処理である。 特に実行速度が重要な for ループに対して、例外のための検査をとても軽い処理で行える。
Calling an end() function would require two calls per iteration. Two calls is much more expensive than one call plus a test for an exception. Especially the time-critical for loop can test very cheaply for an exception.

See also...

# 6.2.1. 比較対象

リストを逆順するクラス Reverse を作って、 それぞれ while 文を使ってリスト内包表記を同じことをして、 どれくらい実行時間に差が出るかを確認します。

def reverse_exception(sequence):
    iterable = ReverseException(sequence)
    # Executing a samething with list comprehension by using while statement.
    # lst = [element for element in iterable]
    lst = []
    iterator = iter(iterable)
    while True:
        try:
            element = next(iterator)
        except StopIteration:
            break
        lst.append(element)
    return lst


def reverse_function(sequence):
    iterable = ReverseFunction(sequence)
    # Executing a samething with list comprehension by using while statement.
    # lst = [element for element in iterable]
    lst = []
    iterator = iter(iterable)
    while has_next(iterator):
        element = next(iterator)
        lst.append(element)
    return lst

# 6.2.2. 測定結果

測定結果は以下の通りです。

$ python 7_2_condition_exception.py 

# Case 0
# ([random.randint(0, 10**n - 1) for i in range(10**n)], ),
reverse_exception                        :   0.0031 [msec]
reverse_function                         :   0.0025 [msec]

# Case 1
# ([random.randint(0, 10**n - 1) for i in range(10**n)], ),
reverse_exception                        :   0.0103 [msec]
reverse_function                         :   0.0118 [msec]

# Case 2
# ([random.randint(0, 10**n - 1) for i in range(10**n)], ),
reverse_exception                        :   0.0809 [msec]
reverse_function                         :   0.1015 [msec]

# Case 3
# ([random.randint(0, 10**n - 1) for i in range(10**n)], ),
reverse_exception                        :   0.8063 [msec]
reverse_function                         :   1.0085 [msec]

# Case 4
# ([random.randint(0, 10**n - 1) for i in range(10**n)], ),
reverse_exception                        :   8.0873 [msec]
reverse_function                         :   9.9956 [msec]

# Case 5
# ([random.randint(0, 10**n - 1) for i in range(10**n)], ),
reverse_exception                        :  82.0660 [msec]
reverse_function                         : 104.7441 [msec]

# 6.2.3. 補足

シーケンスを逆順するイテレータの __next__ メソッドを、 条件分岐と例外でそれぞれ実装しました。 このようにして、希にしか例外が発生しないケースでは例外の方が速そうです。