青ポスの部屋

旅と技術とポエムのブログ

ゼロ除算とどう向き合うべきか

ゼロ除算とは

0でのわり算のことです。

算数では「ゼロでわり算はしてはいけない」と習いますが、数学の世界では「未定義」としてその話はしないというお約束になっています。

コンピュータでのゼロ割り

ゼロでのわり算はやらないというお約束になっていても、膨大な計算が行われるコンピュータプログラムの中では、変数でわり算を行うと必ずと言っていいほどゼロ割りは発生します。

ゼロ割りが起こった場合何が起こるかは言語によります。C++JavaScriptFortranのようにInfinityという特殊な定数が返ってきます。

Welcome to Node.js v12.6.0.
Type ".help" for more information.
> 1.0/0.0;
Infinity

一方、Pythonの組み込みfloat型の場合、ZeroDivisionErrorという例外が起こり処理が止まります。

Python 3.6.7 (v3.6.7:6ec5cf24b7, Oct 20 2018, 13:35:33) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 1.0/0.0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: float division by zero

ただ、非常にややこしいことにPythonネイティブのfloatではゼロ除算に対して例外を投げますが、NumPyのfloatの場合はInfが返ります。

>>> import numpy as np
>>> 1.0/np.float64(0.0)
__main__:1: RuntimeWarning: divide by zero encountered in double_scalars
inf

考察:2つの実装パターンのちがい

C++はInfを返すことからわかるように、Infを返すほうが自然な実装です。ただし、Infの挙動がイメージと違う場合は自分で例外処理を組む必要があります。

また、「ゼロ除算が起こると後の処理にクリティカルな影響が出るという」ようなとき、バグトレースに困ることがあります。例えば「除算の結果をリストのインデックスにして要素を引く」というような場合。IndexErrorが返ってくるが、これがゼロ除算の結果とは直感的にはわかりません。

myindex = int(xx/yy)
res = mylist[myindex]

一方で、Pythonの例外を返すという実装は内部的に例外のチェックが行われているので、例外が起こらない場合でも実行速度に影響を与えています。

そのうえ、結局epsilonとのmaxを取って回避したり、例外処理を実装する必要があるので、ある意味「要らぬおせっかい」とも言えます。

# epsilonで回避する
res = 1.0 / max(xx, sys.float_info.min)

# exceptで例外処理する
try:
    res = 1.0/xx
except ZeroDivisionError:
    res = float("inf")

まとめ

割り算を作るときは、ちゃんとゼロ除算のようなエッジケースを考慮しましょう。(あまりまとまってない)