【Python】list / array / numpy.array を比較してみた
概要
最近、Pythonの標準ライブラリにarray
というデータ型があるのを知りました。
今回は、このarray
の勉強も兼ねて、Pythonのlist
、array
とnumpy.array
の3つを速度、メモリの観点から比較し、それぞれの使用用途について検討してみました。
その結果を備忘録としてこちらに残しておきます。もし間違い等ございましたら、コメントにてご指摘いただけると幸いです。
先に検証の結果を簡潔にまとめると、それぞれの使い道は以下のようになるのかなと考えております。
list
... 配列の各要素に高速にアクセスしたい場合。複数の型のデータを保持したい場合。array
... 配列の要素が単一のデータ型で、配列の各要素へアクセスする必要があり、かつメモリ使用量をメモリ使用量を抑えたい場合。numpy.array
... 配列の各要素にアクセスする必要がなく、メモリ使用量を抑えたい場合。配列の計算を行いたい場合。
array
とは
Pythonのlist
やnumpy.array
は馴染みがあるかと思いますが、array
についてはあまり知られていないかもしれません。
array
はPythonの標準ライブラリで提供されているデータ型で、list
のように配列として複数のデータを保持することができます。
list
と異なる点は、array
には、文字、整数、浮動小数点数しか追加することができず、また一つのarray
には単一の型しか含めることができないという点です。
import array # 要素を整数型に限定した配列を作成 arr = array.array("l", range(10)) print(arr) # array('l', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) # リストと同じように要素の追加もできる arr.append(10) # 異なる型のデータを追加しようとするとエラーが起きる arr.append(1.1) # TypeError
公式ドキュメントでは、このarray
は効率の良い配列と述べられていますが、具体的にどういった点で効率が良いのかは述べられていませんでした。
そこで、速度、メモリの二つの観点から、list
/ array
/ numpy.array
を比較してみました。
検証環境
以下の環境で比較を実施しています
速度面での比較
オブジェクトの生成にかかる時間と、各要素へのアクセスにかかる時間を比較しました。
オブジェクトの生成
jupyterを用いて以下の3つの関数の速度を比較しました
import array import numpy as np n_elements = 10_000_000 def create_list(n_elements: int) -> list: return list(range(n_elements)) def create_array(n_elements: int) -> array.array: return array.array("l", range(n_elements)) def create_numpy_array(n_elements: int) -> np.ndarray: return np.arange(n_elements)
list
オブジェクトの生成速度
%%timeit -n 10 list_ = create_list(n_elements) # 150 ms ± 1.54 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
array
オブジェクトの生成速度
%%timeit -n 10 array_ = create_array(n_elements) # 411 ms ± 760 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
numpy.array
オブジェクトの生成速度
%%timeit -n 10 numpy_array = create_numpy_array(n_elements) # 8.94 ms ± 848 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
オブジェクトの生成速度は、numpy.array
、list
、array
の順にオブジェクトの生成が速いことがわかりました。
array
の生成速度は非常に遅く、numpy.array
と比べて、40倍以上の速度差があるようでした。
各要素へのアクセスの速度
同様にJupyterを用いて要素のアクセス速度の比較を実施しました。
list_ = create_list(n_elements) array_ = create_array(n_elements) numpy_array = create_numpy_array(n_elements)
list
の各要素へのアクセス速度
%%timeit -n 10 for i in list_: pass # 45.6 ms ± 511 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
array
の各要素へのアクセス速度
%%timeit -n 10 for i in numpy_array: pass # 115 ms ± 3.58 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
numpy.array
の各要素へのアクセス速度
%%timeit -n 10 for i in numpy_array: pass # 220 ms ± 3.26 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
list
の各要素へのアクセス速度が最も速く、numpy.array
に比べて5倍近い差があることがわかりました。
list
に比べるとarray
の速度も速くはありませんが、numpy.array
と比べると優位性はありそうです。
使用メモリの比較
続いて使用メモリ量を比較します。
比較にはmemory_profilerを使用しております。
memory_profilerは関数にmemory_profiler.profile
デコレータを追加するだけで、関数ごとのメモリの使用量を簡単に出力してくれます。
import argparse import array import numpy as np from memory_profiler import profile @profile def create(n_elements: int) -> None: list_ = list(range(n_elements)) array_ = array.array("l", range(n_elements)) numpy_array = np.arange(n_elements) def main(): parser = argparse.ArgumentParser() parser.add_argument("n_elements", type=int) args = parser.parse_args() create(args.n_elements) if __name__ == "__main__": main()
検証結果
$ python sample.py 10_000_000 Line # Mem usage Increment Occurrences Line Contents ============================================================= 8 54.7 MiB 54.7 MiB 1 @profile 9 def create(n_elements: int) -> None: 10 441.0 MiB 386.3 MiB 1 list_ = list(range(n_elements)) 11 517.4 MiB 76.4 MiB 1 array_ = array.array("l", range(n_elements)) 12 593.7 MiB 76.3 MiB 1 numpy_array = np.arange(n_elements)
こちらを見るとarray
とnumpy.array
はほとんど同じメモリ使用量で、list
だけその5倍程度のメモリを使用していることがわかりました。
ただし同じコードを何度か実行して見ると、以下のようにarray
の使用メモリが低くなっている結果が得られることがあります。
$ python sample.py 10_000_000 Line # Mem usage Increment Occurrences Line Contents ============================================================= 8 55.0 MiB 55.0 MiB 1 @profile 9 def create(n_elements: int) -> None: 10 441.3 MiB 386.3 MiB 1 list_ = list(range(n_elements)) 11 489.9 MiB 48.6 MiB 1 array_ = array.array("l", range(n_elements)) 12 566.2 MiB 76.3 MiB 1 numpy_array = np.arange(n_elements)
こちらについては原因がよくわかっていないのですが、array.itemsize
を使用してarray
の要素のバイト長を確認すると、8バイトで、
要素数が107であることより、オブジェクトのサイズはおおよそ8 * 107バイト ≒ 76.3MBであると考えられるので、おそらくmemory_profile
側のバグだと考えられます。
ちなみにlist
やnumpy.array
のメモリ使用量に大きくばらつきが出ることはありませんでした。
まとめ
上記の検証結果をまとめたのが以下です。
データ型 | 生成 | 要素へのアクセス | メモリ使用量 |
---|---|---|---|
list |
△ | ○ | × |
array |
× | △ | ○ |
numpy.array |
○ | × | ○ |
これらをもとに考えると、それぞれの使い道は以下のようになりそうです。
list
... 配列の各要素に高速にアクセスしたい場合。複数の型のデータを保持したい場合。array
... 配列の要素が単一のデータ型で、配列の各要素へアクセスする必要があり、かつメモリ使用量をメモリ使用量を抑えたい場合。numpy.array
... 配列の各要素にアクセスする必要がなく、メモリ使用量を抑えたい場合。配列の計算を行いたい場合。
現状すぐにarray
の具体的な用途が思いつくわけではないですが、使える場面はありそうなので、覚えておいて損はなさそうだと感じました。