【python高速化】list処理の高速化方法を比較してみた

pythonのlistの繰り返し処理をする際に使える手軽な高速化方法を紹介します

この記事を読むとわかること・・

  • listの処理を高速化する方法
  • 高速化する際のポイント
おへんじ
pythonは他の言語に比べて処理が遅いから高速化をがんばろう

基本的な繰り返し処理(for文)

まずは基本的なforを使います

今回は準備として

100万人分のテストの得点が入ったlistを作ります

# coding: UTF-8

# 必要なライブラリのインポート
import ramdom

# データの作成(100万人のテストの点数が入ったリストを作成)
test_score_list = [random.randint(0, 100) for i in range(1000000)]

print(test_score_list)
# [95, 87, 31, 99, 75, 35, 38, 36, 91, 31, 23, 36, ・・・・]

print(len(test_score_list))
# 1000000

速度を比較するために実装する処理はこんな感じ

すべて同様の処理をして速度を比較していきます

計測方法は『%%timeit』により計測します

%%timeitの使い方が知りたい方はこちらを参考にしてください

# forを使った処理

judge_list = []  # True, Falseを格納するリストを作成

for test_score in test_score_list:
    if test_score >= 50:  # 50点以上の場合
        judge_list.append(True)
    else:           # 50点未満の場合
        judge_list.append(False)

print(judge_list)
# [True, True, False, True, True, False・・・]

print(len(judge_list))  # 50点以上の人の合計
# 504953

# 計測時間
78.6 ms ± 2.01 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

内包表記による処理1

次は高速化では定番の内包表記による処理を試してみます

おへんじ
内包表記は高速な代わりに可読性が下がるので、コメントなどで補足しておこうね
# 内包表記を使った処理

# True, Falseを格納するリストを作成
over_fifty_list = [True if test_score >= 50 else False for test_score in test_score_list]

print(judge_list)
# [True, True, False, True, True, False・・・]

print(len(judge_list))  # 50点以上の人の合計
# 504953

# 計測時間
49.3 ms ± 494 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

内包表記による処理2(簡略化ver.)

先ほどの処理を実はもう少し簡略することができます

# 内包表記(簡略化ver.)を使った処理

# True, Falseを格納するリストを作成
over_fifty_list = [test_score >= 50 for test_score in test_score_list]

print(judge_list)
# [True, True, False, True, True, False・・・]

print(len(judge_list))  # 50点以上の人の合計
# 504953

# 計測時間
41.6 ms ± 286 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

内包表記で条件式を適用するとbool型(True, False)を要素とするリストを作成することができます

ちなみに、bool型のTrueは1 , Falseは0として扱われます

おへんじ
わざわざTrue, Falseを返すって指定しなくてもいいよ

ジェネレーター式

この場合はTrue, Falseの格納されたリストを作成することはできませんが

50点以上を取った人の数を知ることができます

# ジェネレーター式を使った処理

# True, Falseを格納するリストを作成
judge_generator = (test_score >= 50 for test_score in test_score_list)


print(judge_generator)
# <generator object <genexpr> at 0x10c849ba0>

print(len(judge_generator))  # 50点以上の人の合計
# TypeError: object of type 'generator' has no len()

print(sum(judge_generator))
# 504953

# 計測時間
320 ns ± 10.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

ジェネレーター式はreturnの代わりにyieldなどを内部的に使用しているため・・・(略)

要は必要なときに必要最低限の処理をするためメモリ消費を抑えてくれます

おへんじ
内容表記の[ ]が( )に変わった形がジェネレーター式だよ

ユニバーサル関数(ufunc)

ユニバーサル関数(ufunc)とは・・・

Numpyの配列(ndarray)を要素ごとに処理をして配列(ndarray)で結果を返す関数

Numpyライブラリで提供されている関数ですが、

これは自作関数もユニバーサル関数として使用することができます

おへんじ
Numpyには標準でabs(絶対値を返す関数)やlog(対数を返す関数)などいろんな関数が実装されているよ
# ユニバーサル関数を使った処理

# 自作関数を作成
def judge_func(test_score):
    """
    50点以上か判定する関数
    """
    if test_score >= 50:
        return True
    else:
        return False

# 自作関数をユニバーサル化する
judge_func_uni = np.frompyfunc(judge_func, 1, 1)  # ユニバーサル関数化

# True, Falseを格納するリストを作成
judge_list = [judge_func_uni(test_score) for test_score in test_score_list]

print(judge_list)
# [True, True, False, True, True, False・・・]

print(len(judge_list))  # 50点以上の人の合計
# 504953

# 計測時間
1.32 s ± 11.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
ユニバーサル関数の作り方:np.frompyfunc(自作関数, 引数の数, 帰り値の数)
おへんじ
今回はユニバーサル関数は遅いけど、高速化できる場合もあるよ

map関数による処理

最後はmap関数による処理です

# map関数を使った処理

# True, Falseを格納するリストを作成
judge_list = list(map(lambda x: True if x >= 50 else False, test_score_list))

print(judge_list)
# [True, True, False, True, True, False・・・]

print(len(judge_list))  # 50点以上の人の合計
# 504953

# 計測時間
81.3 ms ± 1.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

map関数はlambdaと組み合わせて使用することが多いです

pandasのdataframeでもmap関数を使用することが結構あります

おへんじ
map関数をサラッとかけるとpythonできる感が少しでるよね

結果まとめ

今回の結果を一覧にしてみます

ユニバーサル関数は遅いので、それを除いた方法をグラフにすると・・・

ジェネレーター式がダントツで早い結果となりました

ただ全ての場合においてジェネレーター式が早いという結論ではないので

注意してください

やりたいことによって最適な方法は変わるので

まずは引き出しを増やすことが大切です

一緒に高速化の引き出しを増やせるように頑張っていきましょう

最新情報をチェックしよう!