みーのぺーじ

みーが趣味でやっているPCやソフトウェアについて.Python, Javascript, Processing, Unityなど.

abs()の解説記事の違和感

Python入門|絶対値を求めるabs関数の使い方

という記事に,Pythonで絶対値を得る方法が解説されていました.

一般的な絶対値の求め方は対象となる数値を二乗した後に平方根を求めることで±記号を取り除きます。

そんな面倒なことをしなくても,負の数ならば-1をかければよいのではないかと思いますが,二乗して平方根を求める方法について掘り下げてみます.

macOS, 64bit cpu, Python3.8で検証しました.

入力が整数だと小数になる問題

import math


def abs_by_sqrt(v):
    return math.sqrt(v**2)


def abs_by_if(v):
    if v < 0:
        return -v
    return v


tests = [
    (0, 0),
    (1, 1),
    (-1, 1),
    (4.98, 4.98),
    (-9.564, 9.564),
    (-5*10**100, 5*10**100)
]

for a, b in tests:
    print(abs_by_if(a), b, abs_by_if(a) == b)
for a, b in tests:
    print(abs_by_sqrt(a), b, abs_by_sqrt(a) == b)

abs_by_sqrt(v)は二乗して平方根を求める関数で,abs_by_if(v)は負の数ならば-1をかける関数です.これをpython3.8で実行してみました.

1 True
1 1 True
4.98 4.98 True
9.564 9.564 True
50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 True
0.0 0 True
1.0 1 True
1.0 1 True
4.98 4.98 True
9.564 9.564 True
5e+100 50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 False

abs_by_if(v)は全ての引数で正しく計算できていますが,abs_by_sqrt(v)は引数が整数でも小数を返します.pythonは気が利くので比較しても等しいと判定されます.しかし,巨大な整数の場合は比較結果が誤っています.

そもそもpython3で扱える整数の最大値はメモリの容量に依存します.

整数の上限がなくなったため、sys.maxint 定数は削除されました。しかしながら、通常のリストや文字列の添え字よりも大きい整数として sys.maxsize を使うことができます。 sys.maxsize は実装の "自然な" 整数の大きさに一致し、同じプラットフォームでは (同じビルドオプションなら) 過去のリリースの sys.maxint と普通は同じです。

https://docs.python.org/ja/3/whatsnew/3.0.html#integers

とはいえ,math.sqrt()によって,floatとして扱われるため,64bit浮動小数演算を行うことにより,floatの制約を受けます.

浮動小数点型はたいていは C の double を使って実装されています.

https://docs.python.org/ja/3/library/stdtypes.html#numeric-types-int-float-complex

以上より,上記のような5×10100のように,floatで扱える範囲で巨大な整数の絶対値を計算しようとすると,計算結果に誤差を含むようになります.不便です.

入力が巨大過ぎると計算できない問題

上記のスクリプトを以下に変更して実行してみます.

tests = [(-5*10**400, 5*10**400)]
50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 True
OverflowError
int too large to convert to float

abs_by_if(v)は正しく計算できますが,abs_by_sqrt(v)はOverflowErrorとなります.

64bit cpuで扱えるfloatの最大値が10308程度のためこれを超えると計算できません.Pythonのとても大きな整数を扱えるというメリットが消失しています.やはり不便です.

パフォーマンスが悪い

上記のスクリプトの速度を計測してみます.

r1 = timeit.timeit('abs_by_sqrt(-1024)', globals=globals(), number=10000)
r2 = timeit.timeit('abs_by_if(-1024)', globals=globals(), number=10000)
r3 = timeit.timeit('abs(-1024)', globals=globals(), number=10000)
print(r1)
print(r2)
print(r3)
print(r1/r3)
print(r2/r3)

これを実行したところ,以下のような結果でした.

0.004863887000000001
0.0011133859999999975
0.0005867680000000014
8.289284691735045
1.8974892973031843

つまり,abs_by_sqrt(v)abs(v)と比較して8倍以上遅いようです.abs_by_if(v)は1.8倍程度遅く,おそらくifによる条件分岐が原因と思います.

結論

黙ってabsを使おう.