はじめに
Pythonは可読性が高く、ライブラリが豊富というメリットがあります。一方で、遅いから使いたくないと言われることもあります。たしかにコンパイル方式の言語に比べれば遅い部類ではあります。しかし、実際のところではpython的な書き方を分からず、他の言語の流儀でコーディングをしたことに起因して処理時間がかかっているケースも見受けられます。
例えば総当り計算や画像処理に多重ループを使う、といったものです。本記事では多重ループを例にして、python的な書き方に書き換えることで、どのような変化があるか紹介します。それを通してpython的な書き方を学ぶ意義について説明したいと思います。
対象の読者
他の言語からpythonに入った方(組み込みC言語からpythonに入った過去の私)
例: 総当りの計算をする
数列に対して、それぞれの要素同士に対して総当りで何らかの計算をするコードについて考えます。
以下は入力された数列に対して自身を含むそれぞれの要素との差を計算しています。outputの1行目は0-0, 0-1, 0-2を計算したもの、2行目は1-0, 1-1, 1-2を計算したもの、3行目は2-0, 2-1, 2-2を計算したものです。
input:
[0, 1, 2]
output:
[[0, -1, -2]
[1, 0, -1]
[2, 1, 0]]
for文を使うパターン
このようなとき、各要素で計算することから下記のように計算する方もいるのではないでしょうか?なお、計算時間の変化が分かりやすいように、数列の長さは指数的に増えるように設定しています。
import time
DIGITS = 5 # 計算したい桁数
def calc_in_for(l):
"""forループを使った総当りの計算
"""
output_l = []
for i in l:
temp = []
for j in l:
temp.append(i - j) # 計算部分
output_l.append(temp)
return output_l
for i in range(DIGITS):
# 計算する数列の要素数
num = 10**i
# 数列を作成
input = range(num)
# forループによる計算
start = time.time()
output = calc_in_for(input)
calc_time_for = time.time()-start
print(f"{num:>5}: {calc_time_for:>10.3f}ms")
実行結果
私の環境では以下のようになりました。
計算する数列の長さ | for文の計算時間[ms] |
---|---|
1 | 0.000 |
10 | 0.000 |
100 | 0.001 |
1,000 | 0.093 |
10,000 | 10.610 |
python的な書き方: 多重ループの代わりにnumpyを使う
同じ処理はnumpyを使うことでfor文を使わずに書くことができます。numpyは、多次元配列の計算を高速に処理するためのライブラリと考えてください。以下は、numpyを使って処理時間を改善する例です。
処理の詳細はまとめの後に記載します。ただしnumpyの基本的な説明は省略します、ご了承ください。
import numpy as np
def calc_in_np(l):
"""numpyを使った総当りの計算
"""
arr = np.array(l)
return arr[:, np.newaxis] - arr[np.newaxis, :] # 計算部分
実行結果
numpyを使用することで処理時間を1桁程度短縮することができました。コード量も減り、可読性も向上しています。
計算する数列の長さ | for文の計算時間[ms] | numpyの計算時間[ms] |
---|---|---|
1 | 0.000 | 0.000 |
10 | 0.000 | 0.000 |
100 | 0.001 | 0.000 |
1,000 | 0.093 | 0.011 |
10,000 | 10.610 | 1.673 |
他にもあるメリット
今回は計算対象が1次元でした。実際の処理では画像処理や2次元座標上にある点間の距離計算など、多次元の計算もあるかと思います。こういった場合、for文による多重ループを使うとコードが複雑になり可読性も低下しますが、numpyを使えばシンプルに書くことができます。
まとめ
(私も含め)もともと別の言語をやっていた方は、つい慣れた書き方で処理を書いてしまいがちです。しかし、例に挙げたように良い書き方というものは言語(もしくはバージョン)が変われば変化します。ビジネスではより速く、より可読性が高く、より保守性が高いコードが直接的・間接的に利益に繋がることから経験則だけに頼らず、python(現在自分が触っている言語)的に良い書き方を学ぶ、というのは意義があります。
numpyを使った書き方では何をやっていたか
まず、np.newaxis
を使うことで次元を一つ追加しています。これにより入力された1次元の情報は2次元の情報に変換されます。
import numpy as np
arr = np.array([0, 1, 2])
print(arr)
# [0 1 2]
print(arr.shape)
# (3,)
arr1 = arr[:, np.newaxis]
arr2 = arr[np.newaxis, :]
# 内容の確認
print(arr1)
#[[0]
# [1]
# [2]]
print(arr2)
# [[0 1 2]]
# 形状の確認
print(arr1.shape)
# (3, 1)
print(arr2.shape)
# (1, 3)
arr1とarr2は形状が異なります。ここがポイントです。arr1とarr2の型はndarray
なのですが、ndarray
の計算では形状が同じくなるように自動的に形状の変換が行われます。これによりfor文を使わず、一発で計算ができるわけですね。
[[0] - [[0 1 2]] -> [[0 0 0] - [[0 1 2]] => [[0 -1 -2]
[1] [1 1 1] [[0 1 2]] [1 0 -1]
[2]] [2 2 2]] [[0 1 2]] [2 1 0]]