Pythonのコードを速くする7つの方法

Python Scripting for Computational Science (Texts in Computational Science and Engineering)
Python Scripting for Computational Science (Texts in Computational Science and Engineering)
Springer 2009-02-13
売り上げランキング : 40536

おすすめ平均 star
star理工系のためのPython教本

Amazonで詳しく見る
by G-Tools

Python Scripting for Computational Science』8.10.3節に、pythonコードを最適化(optimazation)する方法の例が15ほどまとめられている。
この本の目的自体が、pythonを科学技術計算で利用することを紹介するものである。そのためそこで紹介されている15の方法においては、numpyライブラリを使用することが前提であったり、CやFortranコードと組み合わせてpythonを使うときの最適化方法であったりして、科学技術計算以外でpythonを使う人たちにはそれほど有用であるとは思えないものもいくつかある。
それらの方法を差し引いて、すべてのpythonユーザの役に立つと思われる、コードのスピードアップ方法を7つ選んで、ここに考察を加えてまとめておく。

1. Avoid explicit loops --- use vectorized NumPy expressions instead.

繰り返し処理を行うときに、for文のループは使わずにベクトル化したNumPy表式を使うと速くなる。ここでのベクトル化とは、スカラー値ではなく配列を関数の引数として取れるようにすることを意味する。

例えば、

from numpy import *

def somefunc(x):    # 関数の定義
    if x < 0:
        return x
    else:
        return sin(x)

a = 0.0
for e in range(5000000): # 単純なfor文
    a += somefunc(e)
print a

この単純なループだと、計算時間が15.579秒かかる。一方で、

from numpy import *
import time

def somefunc(x):
    if x < 0:
        return x
    else:
        return sin(x)

somefuncv = vectorize(somefunc) # ベクトル化

a = 0.0
x0 = linspace(0, 5000000-1, 5000000) # 配列作成
a= somefuncv(x0)  # ベクトル化した関数、配列を返す
print sum(a)   # 配列の要素の和を取る

では、計算時間は13.000秒かかる。17%のスピードアップ。この例だと有難味が微妙だな。

2. Avoid module prefix in frequently called functions.

モジュールをインポートしてその関数を繰り返し使う場合、毎回、mod.funcとすると遅くなる。つまり、

import mod
for x in hugelist:
    mod.func(x)

ではなく、

from mod import func
for x in hugelist:
    func(x)

がベター。もしくはmodule prefixは最小限に抑える。

3. Plain functions are called faster than class methods.

クラスメソッドを呼び出すより、普通の関数を呼び出す方が速い。なぜかというと、クラスメソッドの場合、インスタンスメソッドを新しい変数に入れて、その変数が呼ばれるようにするため、遅くなる。

4. Inlining functions speeds up the code.

関数を呼び出すのは負荷がかかること、というのはどんな言語も共通していることなのだな。関数呼び出しよりもできればインライン展開することを心掛ける。

5. Avoid using NumPy functions with scalar arguments.

引数がスカラー値の場合には、NumPyの関数を使うべきではない。例えば、math.sin(x)はnumpy.sin(x)よりはるかに速い。なので、

from numpy import *
from math import *
...
y=sin(x)
...

としたときは気をつけないと、無意識のうちにNumPyのsinを使うことになる。

6. Use xrange instead of range

for文でループさせるとき、rangeよりxrangeの方がメモリもCPU時間も節約できる。いつもxrangeにするとよい。これは昔、このブログで親切な人からコメントをいただいたので知っていた。

7. if-else is faster than try-except

例題。

def f1(x):
    if x > 0:
        return sqrt(x)
    else:
        return 0.0

def f2(x):
    try:
        return sqrt(x)
    except:
        return 0.0

この例の関数f1とf2を使うことを考えたとき、関数f2で、tryが成功してexceptまで行かないときには、関数f1とf2の差はあまりないらしい。exceptブロックを通ると負荷がかかるので、例外が頻繁に起こる場合には、関数f1のようにif-elseで書きなおした方が良い。

以上、たぶん汎用的に使える7つの方法です。
残りのいくつかの(特殊な)optimazationに興味がある方は、8.10.3節をお読みください。