初めに
今回は少し雑談の多い内容になっております。すぐに解決方法を知りたい方は事象と解決方法の章をご覧いただければと思います。
動的型付けって便利ですよね
もともと静的型付け言語であるC言語やJavaからプログラミングに入門した身としては、独学でPythonを始めたとき、型宣言がないことに憤りを感じました。こういった感覚を持つ人は多分一定数いると思います。少なくとも大学時代、周囲にそういうことを言う人は一定数いました。
数学的には同じ実数であってもプログラミングの世界では32bitの整数で扱うのか、浮動小数で扱うのか、倍精度で扱うのか、BigDecimalで扱うのかで演算結果は変わります。
型宣言をせずに変数を宣言するときはいつも不安になります。Pythonには型ヒントという機能がありますが、型ヒントは型宣言ではないので、型ヒントによって型を指定しているわけではありません。(型ヒントは静的型チェッカーと合わせて使うことで威力を発揮します)
しかし、Pythonのコードに少し慣れてくると、型推論の機能の凄さに自然ともたれ掛かってしまうようになっていました。例えば32bitの符号付整数の最大値(2147483647)を宣言しておいて、そこに1を加えるというような操作しても、Pythonはなぜか自然と正しい答えを返してくれます。
C言語でこんなことをやったらOverflowして-2147483648が返ってくるでしょう。そう考えてみると頭のいい人が用意してくれたメソッドで適切な型推論をしてくれる動的型付け言語って便利ですよね。
ただ、型推論も完璧ではないことをふとした偶然から見つけたので記事にしました。もちろん型推論が間違うケースはほかにも様々あると思います。そういった背景から静的型付けも近年見直されているというようなネット記事も見かけます。
ここではかなりはケースを絞ってお話します。環境依存の話ですので、そもそもなにも困っていないという人がほとんどかと思いますが、参考になれば幸いです。
仮想環境のメリット
Anacondaは仮想環境でPythonを動かすことができるので初心者(もちろんここに私も含まれます)にもおススメな便利なツールです。なぜ仮想環境が良いのかというと、いざというときに気楽に初期化できるからです。
Pythonは多数のライブラリをimportして使うことを前提としていますから、おなじみのpipコマンドでどんどんいろいろな機能をinstallしていくわけですが、そうこうしているうちにライブラリ同士の依存性等によって問題を生じることがあります。
特にNumPyのVersionが新しいと正常に動かないライブラリがあり、こういったことは日々起こります。仮想環境でプログラムを実行し、いざというときに最初からやり直すことができる、またローカル環境を下手にいじってマシンをクラッシュさせないというのは安心です。
事象
今回取り上げるのは仮想環境でNumPyのメソッドがOverflowしてしまったケースです。
次のようなケースを実行すると64bit Windows、Mac、Linuxなど、ほとんどの環境で正常に動作すると思います。一方は標準のsum()関数でもう一方はnp.sum()関数です。求めるべき答えは2147483647+1で2147483648です。
import numpy as np
n = 2147483647
m = 1
arr = [n,m]
print(np.sum(arr))
print(sum(li))
しかし32bit Windowsで行うとnp.sum(arr)のみがOverflowして-2147483648と出てきます。これはおそらくNumPyの型推論が通常はデフォルトでint64(Pythonでは32bitでなくてもdoubleではなくintと言います)なのですが、それが環境に依存しているためだと思われます。32bit Windowsのデフォルトはint32です。厄介なのは仮想環境でのデフォルトです。私も64bit Windows上で動かしていたのですが、Anacondaの仮想環境では32bitだったようでOverflowが起こってしまいました。
解決方法
何とかする方法はないかと思って調べてみるとNumPyで型を表示したり、型を明示的に与える方法が分かりました。
例えば0から100000000(一億)までの整数が代入されたnumpy.ndarrayを作りその要素を全て足し合わせるというシチュエーションを考えてみましょう(もちろんこういう場合はいきなり愚直に計算するのではなく、よく考えてエレガントに解くのが大切だというようなことを偉大なる計算機械科学者Dijkstraも確か言っています)。
まあ、これはあくまでテストです。答えはもちろん、
$$
\sum_{k=1}^{100000000} k =\frac{100000000 \cdot 100000001}{2} = 5000000050000000
$$
となります。これをデフォルトが32bitの環境で
# 32bitだとオーバーフローする
import numpy as np
array = np.arange(10 ** 8 + 1)
print(array.dtype)
print(np.sum(array))
などと書いていまうと、
int32
987459712
と表示され、正しく計算できません。dtypeはNumPyの型を表示してくれます。ここで、numpy.ndarrayの最大値はint32以下で、和はint32よりおおきくint64なので理想的にはそれぞれ別に指定してやるのがよいということになります。そんな方法があるのでしょうか。あります。dtype=np.int32やdtype=np.int64というようにしてNumPyのメソッドにdtypeを渡してあげてください。
# オーバーフローを避ける方法
import numpy as np
import time
start_time = time.time()
array = np.arange(10 ** 8 + 1, dtype=np.int32)
print(np.sum(array, dtype=np.int64))
end_time = time.time()
execution_time = end_time - start_time
print(execution_time)
このようにします。ついでに実行時間も表示するようにしました。結果は、
5000000050000000
0.18909001350402832
と無事に正しい値が計算できました。実行時間は約200ミリ秒。実行時間比較のためにnumpy.ndarrayもint64にした結果、実行時間はおよそ300ミリ秒でした。
それでは今日はこの辺で。今回の最後のケースのようにC言語で書かれた高速なNumPyを使いnumpy.ndarrayで遅いfor文のコードを改善するパターンなどについてもそのうち書きたいと思っています。