このBlogは移転しました。今後は aish.dev を御覧ください。

RecursionError は RecursionError とは限らない、という話

先日、 Pythonjp Discordサーバ の初心者部屋で、こんな質問 があった。

次のようなコードが RecursionError 例外を出して困っているという。

if a:
    ...
elif b:
    ...
elif c:
    ...
elif d:
    ...

のように、数千個の elif が並んでるコードだ。

多くの場合、RecursionError は、関数が自分自身を繰り返し呼び出す 再帰呼び出し で発生する。こんな感じだ。

>>> def f():
...     f()
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
  File "<stdin>", line 2, in f
  File "<stdin>", line 2, in f
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
>>> 

しかし、問題のコードには、再帰呼出しはもちろん関数呼び出しも存在しない。なぜ RecursionError 例外が発生するのだろうか?

関数呼び出しのないRecursionError

問題のコードを実行すると、次のようなエラーが出る。

$ python3 foo.py
RecursionError: maximum recursion depth exceeded during compilation

実は、このエラーはPythonスクリプトを実行して発生しているのではない。ソースコードコンパイルするときに、Python言語のコンパイラで発生している。

Pythonは、ソースコードコンパイルするときに 抽象構文木(AST) というツリー状のデータ構造を作る。この AST を処理するときに再帰的に処理を行うため、ツリーのネストが深すぎると RecursionError 例外が発生する。

このケースでは if 〜 elif 〜 elif 〜 が大量に並んでおり、if 文の子ノードとして elif ノードが作られ、その elif ノードの子ノードとしてまた elif ノードが作られ、その elif ノードの子ノードとして…… という風に、elif の数だけツリーが深くなっている。

このため、ソースコードコンパイルする過程で行われる再帰呼び出しの回数が制限を超えてしまい、RecursionError 例外 が発生してしまうのだ。

対処方法

関数呼び出しがまったく存在しないソースコードであっても、Pythonコンパイラの制約により RecursionError 例外が発生してしまうことがある。この例では極端に長い if 〜 elif 〜 elif 〜 ブロックだったが、他にも ((((......(a)+1)+1)+1)......) のようなコードでも発生する場合がある。

このようなエラーが出てしまったら、ASTが深くなりすぎないようなコードに修正する必要がある。

if 〜 elif 〜 elif 〜 ブロックなどが長すぎる場合は、短く分割する。長すぎる式は複数行に分割する、などの変更ですむので、対処はそれほど難しくない場合が多いと思う。