pythonにおけるlist、numpy.arrayの複製速度(&代入速度)

先日投稿した「[pythonにおけるlist、numpy.arrayの複製、初期化(0埋め)](https://qiita.com/tmurakami1234/items/558401371fad8d66786e)」という記事でlist、ndarrayの幾つかの複製方法について触れましたが、今回の記事では、各複製方法の速度を測定しました。

# 結果

1次元list・ndarrayのそれぞれの場合で、同じ長さの配列を用意し、それの複製速度を測定しました。[前回の記事](https://qiita.com/tmurakami1234/items/558401371fad8d66786e)ではcopyモジュールのdeepcopyは使いませんでしたが、今回は方法(6)として、deepcopyによる複製速度も測定しました。

## 1次元listの複製

長さ1000の1次元list aを用意して、方法(2)〜(6)のそれぞれの方法でaを複製する関数を定義し、処理速度を測定した結果が次の表です。

|番号|方法|処理速度の平均[秒]|標準偏差[秒]|
|---|---|---|---|
|(2)|copy_a = a[:]|0.00352|0.000101|
|(3)|copy_a = a.copy()|0.00353|0.000115|
|(4)|copy_a = list(a)|0.00344|0.000156|
|(5)|copy_a = [elem for elem in a]|0.017|0.000632|
|(6)|copy_a = deepcopy(a)|0.405|0.0259|

list aが高次元配列の場合、方法(6)のdeepcopyは複製に重宝しますが、実行速度は最も遅い結果となりました。

## 1次元ndarrayの複製

長さ1000の1次元ndarray aを用意して、方法(3)〜(6)のそれぞれの方法でaを複製する関数を定義し、処理速度を測定した結果が次の表です。

|番号|方法|処理速度の平均[秒]|標準偏差[秒]|
|---|---|---|---|
|(3)|copy_a = a.copy()|0.00053|6.17e-05|
|(4)|copy_a = np.array(a)|0.000568|8.08e-05|
|(5)|copy_a = np.array([elem for elem in a])|0.093|0.00314|
|(6)|copy_a = deepcopy(a)|0.00172|0.000179|

aがlistのときと比べると、ndarrayの方が全体的に複製速度が速いですね。方法(5)は一度listを作ってndarrayに変換しているため1次元listの複製のときよりも遅い結果となっています。

# 測定方法
次のスクリプトを実行して処理速度の測定を行いました。

```python
from mytools import measure_time
import numpy as np
from copy import deepcopy


def do_nothing():
    pass

def copy_list2(_list):
    new_list = _list[:]
    del new_list

def copy_list3(_list):
    new_list = _list.copy()
    del new_list

def copy_list4(_list):
    new_list = list(_list)
    del new_list

def copy_list5(_list):
    new_list = [elem for elem in _list]
    del new_list

def copy_list6(_list):
    new_list = deepcopy(_list)
    del new_list

def copy_array3(_array):
    new_array = _array.copy()
    del new_array

def copy_array4(_array):
    new_array = np.array(_array)
    del new_array

def copy_array5(_array):
    new_array = np.array([elem for elem in _array])
    del new_array

def copy_array6(_array):
    new_array = deepcopy(_array)
    del new_array

if __name__ == '__main__':
    N = 1000
    _list = list([0 for _ in range(N)])
    _array = np.array([0 for _ in range(N)])

    measure_time(do_nothing)

    measure_time(copy_list2, _list)
    measure_time(copy_list3, _list)
    measure_time(copy_list4, _list)
    measure_time(copy_list5, _list)
    measure_time(copy_list6, _list)

    measure_time(copy_array3, _array)
    measure_time(copy_array4, _array)
    measure_time(copy_array5, _array)
    measure_time(copy_array6, _array)
```

次が、上記スクリプト中の`measure_time`関数の定義です。

```python
import sys
import time
import numpy as np


def measure_time(func, *args, iteration=1000, samples=1000):
    _tuple = (func.__name__, iteration, samples)
    print('Function: {}\nIteration: {}\nSamples: {}'.format(*_tuple))
    series = []
    for i in range(samples):
        sys.stdout.write('\rProgress: [{}/{}]'.format(i, samples))
        start = time.time()
        for j in range(iteration):
            func(*args)
        series.append(time.time()-start)
    print('\rMean: {}, SD: {}\n'.format(np.average(series), np.std(series)))
```

`measure_time`関数は、引数として渡された関数がiteration回実行されるのに掛かる時間をsamples回記録して、処理時間の平均と標準偏差を標準出力します。以下が`measure_time`の実行例です。

```
Function: do_nothing
Iteration: 1000
Samples: 1000
Mean: 9.026002883911132e-05, SD: 9.253773932953281e-06

Function: copy_list2
Iteration: 1000
Samples: 1000
Mean: 0.0034766535758972168, SD: 6.660195155484076e-05
```

# 備考
上記スクリプト中の関数`copy_array4`では、ndarrayを受け取って`np.array()`により新たにndarrayオブジェクトを作っていますが、`copy_array4`に上記スクリプト中の_listを渡した場合の処理速度を測定してみました。

```
Function: copy_array4
Iteration: 1000
Samples: 1000
Mean: 0.04266641592979431, SD: 0.00023159095884393903
```

ndarrayを渡されたときよりも100倍程遅いですね。加えて、関数`copy_list4`に同じ_listを渡したときよりも10倍程遅いです。

逆に、関数`copy_list4`にスクリプト中の_arrayを渡した場合の処理速度を測定してみましょう。

```
Function: copy_list4
Iteration: 1000
Samples: 1000
Mean: 0.029821698427200317, SD: 0.004067780773085223
```

`copy_array4`に_listを渡した場合の平均処理速度と同じオーダーになりました。

listオブジェクトを引数に`np.array()`でndarrayオブジェクトを作ったり、ndarrayオブジェクトを引数に`list()`でlistオブジェクトを作るのって時間掛かるんですね。

# 追記(代入速度の測定)

興味本位で、次のスクリプトにより1次元list・ndarrayの各要素を書き換える操作の処理速度を測定してみました。

```python
from mytools import measure_time
import numpy as np


def for_only(N):
    for n in range(N):
        pass

def substitute_list(_list, N):
    for n in range(N):
        _list[n] = 0
    del _list

def substitute_array(_array, N):
    for n in range(N):
        _array[n] = 0
    del _array
    
if __name__ == '__main__':
    N = 1000
    _list = list([0 for _ in range(N)])
    _array = np.array([0 for _ in range(N)])

    measure_time(dummy, _list, N)
    measure_time(substitute_list, _list, N)
    measure_time(substitute_array, _array, N)
```

以下がこのスクリプトの実行結果です。

```
Function: for_only
Iteration: 1000
Samples: 1000
Mean: 0.01462605857849121, SD: 0.0008302230687152986

Function: substitute_list
Iteration: 1000
Samples: 1000
Mean: 0.02975408983230591, SD: 0.0016898342277376784

Function: substitute_array
Iteration: 1000
Samples: 1000
Mean: 0.0648690721988678, SD: 0.00520460908502261
```

関数`for_only`はfor文をただ回すだけの関数なので、for文の実行時間文を差し引くと、listオブジェクトの要素を書き換える方がndarrayオブジェクトの場合よりも3倍程速い結果となります。

# リンク

* [Pythonのリスト内包表記の速度-Qiita](https://qiita.com/intermezzo-fr/items/43f90e07e4cebe63aeb6)
* [Pythonの配列アクセス速度比較-Qiita](https://qiita.com/pegasusBS15/items/75da968c9d3ee285f851)
* [Pythonを速くしたいときにやったこと-Qiita](https://qiita.com/shaka/items/f180ae4dc945dc7b9066)