【Python】リストの要素を削除するforループの書き方【一般的な方法と高速な方法】

Pythonのリスト(list)から『条件を指定して要素を削除』するには、リストをfor文にかける必要があります。そのコード例と速さの比較を紹介します。

for文で問題になるのが、インデックスエラー (IndexError: list assignment index out of range) です。

ループ中に要素を削除したことがfor文に影響して、エラーが発生します。

そこで、エラーを出さない削除方法を6種類しらべて比較しました。

大きく分けて、リストのコピーと .remove() を使った『一般的な削除方法』と、要素のインデックスを指定して del文 や .pop() で削除していく『高速な削除方法』の2つがあります。

どの方法を使ったらいいのか?

結論

通常は for t in v[:]: v.remove(t) を使う

通常は『一般的な削除方法』の for t in v[:]: v.remove(t) で大丈夫です。

構文が簡単で便利です。

ただし、リストの要素が増えると、一気に遅くなります。

また、リストのコピーを作る関係で、メモリの消費が少し増えます。

リストの要素が1万個以下の小さなリストに向いていると思います。

大きなリストでは for i in reversed(range(len(v))): del v[i] を使う

もし、リストの要素が1万個以上あって『高速な削除方法』が必要なら、要素のインデックスを指定して del文 で削除していく方法をお勧めします。

具体的には、for i in reversed(range(len(v))): del v[i] で削除していく方法です。

この方法は、『構文の分りやすさ』と『実行の速さ』から、おすすめのリスト要素削除方法です。

また、for文を動かす時にリストのコピーを作らないので、メモリの消費も少なくて済みます。

ただし、こちらは『リストの後ろから要素を削除していく』カタチになります。

これは、要素の削除でfor文に影響を与えないための工夫です。

以下、『一般的な削除方法』と『高速な削除方法』の解説と、速さの比較です。

スポンサーリンク

一般的な削除方法

Pythonの公式マニュアルにある『一般的な削除方法』です。

for文にリストのコピーを渡して、リムーブメソッド .remove() で削除していく方法です。

普通、リストの要素を削除するときは、こちらの方法でいいと思います。

Pythonの公式マニュアルは 複合文 (compound statement) for 文 にあります。注釈の中で、リストのコピーを使った方法が紹介されています。

リストの要素を削除するコード例(スライスとリムーブ)

for文にリストのコピーを渡して、リムーブで削除していくコードです。

"""一般的な削除方法"""
print('for t in v[:]: v.remove(t)')

v = list(range(5))
print('t %s' % v)

for t in v[:]:
    v.remove(t)
    print('%s %s' % (t, v))

print('end t: %s len(v): %d' % (t, len(v)))

ループ中の削除でfor文に影響が出ないように、スライス [:] でコピーを作っているところがポイントです。

実行結果です。

リムーブ .remove(t) の繰り返しで、t がリストから順に削除されています。

for t in v[:]: v.remove(t)
t [0, 1, 2, 3, 4]
0 [1, 2, 3, 4]
1 [2, 3, 4]
2 [3, 4]
3 [4]
4 []
end t: 4 len(v): 0

もし、for文でリストのコピーを作らずに削除していくと、以下のインデックスエラーが出ます。

IndexError: list assignment index out of range

for文には v ではなく、v[:] を渡すことで、エラーが出なくなります。

 

さて、『一般的な削除方法』は一番簡単です。

しかし、大きなリストでは非常に遅くなります。

また、for文を動かす時にリストの浅いコピーを作るので、メモリの消費が少し増えます。

浅いコピーでメモリが2倍になったりはしませんでしたが、元のリストの2割増しくらいのメモリ消費にはなっていました。

そこで、大きなリストで要素を削除するときは、次の『高速な削除方法』をおすすめします。

高速な削除方法

forループでリストの後ろからアクセスしていって、del文やポップ .pop() で削除していく方法です。

リストの後ろから削除していくと、forループの進行方向にある要素のインデックスが狂わないので、インデックスエラー(IndexError)が発生しません。

また、逆順でアクセスするためのインデックスは、レンジ range() やリバースド reversed() で生成するので、メモリをほとんど消費しませんでした。

要素を削除するdel文とポップも、リムーブより高速でした。

ここでは、『インデックスの生成方法』と『要素の削除方法』を組み合わせて、5種類の方法を試しましたので紹介します。

どれを使ったらいいのかですが、タイムイット(timeit)で時間を計ったら大差がなかったので、構文の好みで選べばいいと思います。

おすすめの方法は、リバースドとdel文を使った for i in reversed(range(len(v))): del v[i] です。

コードは長いですが、読めば分かるところが好きです。

スポンサーリンク

リストの要素を削除するコード例(リバースドとdel文)

リストの長さで作ったレンジを、リバースドで逆順にしています。

要素の削除にdel文を使っています。

N_MAX = 5

print('for i in reversed(range(len(v))): del v[i]')
v = list(range(N_MAX))
print('i %s' % v)

for i in reversed(range(len(v))):
    del v[i]
    print('%d %s' % (i, v))

print('end i: %s len(v): %d' % (i, len(v)))

実行結果です。

for i in reversed(range(len(v))): del v[i]
i [0, 1, 2, 3, 4]
4 [0, 1, 2, 3]
3 [0, 1, 2]
2 [0, 1]
1 [0]
0 []
end i: 0 len(v): 0

リストの要素を削除するコード例(スライスとdel文)

リストの長さで作ったレンジを、スライスで逆順にしています。

要素の削除にdel文を使っています。

N_MAX = 5

print('for i in range(len(v))[::-1]: del v[i]')
v = list(range(N_MAX))
print('i %s' % v)

for i in range(len(v))[::-1]:
    del v[i]
    print('%d %s' % (i, v))

print('end i: %s len(v): %d' % (i, len(v)))

実行結果です。

for i in range(len(v))[::-1]: del v[i]
i [0, 1, 2, 3, 4]
4 [0, 1, 2, 3]
3 [0, 1, 2]
2 [0, 1]
1 [0]
0 []
end i: 0 len(v): 0

リストの要素を削除するコード例(レンジとdel文)

リストの長さでレンジを作るときに、start, stop, step を指定して逆順にしています。

要素の削除にdel文を使っています。

N_MAX = 5

print('for i in range(len(v)-1, -1, -1): del v[i]')
v = list(range(N_MAX))
print('i %s' % v)

for i in range(len(v)-1, -1, -1):
    del v[i]
    print('%d %s' % (i, v))

print('end i: %s len(v): %d' % (i, len(v)))

実行結果です。

for i in range(len(v)-1, -1, -1): del v[i]
i [0, 1, 2, 3, 4]
4 [0, 1, 2, 3]
3 [0, 1, 2]
2 [0, 1]
1 [0]
0 []
end i: 0 len(v): 0

リストの要素を削除するコード例(リバースドとポップ)

リストの長さで作ったレンジを、リバースドで逆順にしています。

要素の削除にポップ .pop() を使っています。

N_MAX = 5

print('for i in reversed(range(len(v))): v.pop(i)')
v = list(range(N_MAX))
print('i %s' % v)

for i in reversed(range(len(v))):
    v.pop(i)
    print('%d %s' % (i, v))

print('end i: %s len(v): %d' % (i, len(v)))

実行結果です。

for i in reversed(range(len(v))): v.pop(i)
i [0, 1, 2, 3, 4]
4 [0, 1, 2, 3]
3 [0, 1, 2]
2 [0, 1]
1 [0]
0 []
end i: 0 len(v): 0

リストの要素を削除するコード例(スライスとポップ)

リストの長さでレンジを作るときに、start, stop, step を指定して逆順にしています。

要素の削除にポップ .pop() を使っています。

N_MAX = 5

print('for i in range(len(v)-1, -1, -1): v.pop(i)')
v = list(range(N_MAX))
print('i %s' % v)

for i in range(len(v)-1, -1, -1):
    v.pop(i)
    print('%d %s' % (i, v))

print('end i: %s len(v): %d' % (i, len(v)))

実行結果です。

for i in range(len(v)-1, -1, -1): v.pop(i)
i [0, 1, 2, 3, 4]
4 [0, 1, 2, 3]
3 [0, 1, 2]
2 [0, 1]
1 [0]
0 []
end i: 0 len(v): 0

タイムイットで時間を計る

リストから要素を削除するのに要する時間を、タイムイット(timeit)で計りました。

スポンサーリンク

時間を計ったコード

6種類の削除方法を関数にして、10万個の要素を持つリストを渡しています。

100万個で実行したら『一般的な削除方法』が全然終わらなかったので、N_MAX=10万個に減らしました。

"""リストから要素を削除するのに要する時間を計る"""
import timeit

# 一般的な削除方法
def list_del_a(n_max):
    v = list(range(n_max))
    for t in v[:]:
        v.remove(t)
    return


# 高速な削除方法
def list_del_b(n_max):
    v = list(range(n_max))
    for i in reversed(range(len(v))):
        del v[i]
    return


def list_del_c(n_max):
    v = list(range(n_max))
    for i in range(len(v))[::-1]:
        del v[i]
    return


def list_del_d(n_max):
    v = list(range(n_max))
    for i in range(len(v)-1, -1, -1):
        del v[i]
    return


def list_del_e(n_max):
    v = list(range(n_max))
    for i in reversed(range(len(v))):
        v.pop(i)
    return


def list_del_f(n_max):
    v = list(range(n_max))
    for i in range(len(v)-1, -1, -1):
        v.pop(i)
    return


# 繰り返し回数 1回
NUMBER = 1

# for文のリストの長さ 10万
N_MAX = 100000

# 変数名と関数名の辞書を取得
g = globals()


ta = timeit.timeit('list_del_a(N_MAX)', globals=g, number=NUMBER)
print('%f seconds  for t in v[:]: v.remove(t)\n' % ta)

tb = timeit.timeit('list_del_b(N_MAX)', globals=g, number=NUMBER)
print('%f seconds  for i in reversed(range(len(v))): del v[i]\n' % tb)

tc = timeit.timeit('list_del_c(N_MAX)', globals=g, number=NUMBER)
print('%f seconds  for i in range(len(v))[::-1]: del v[i]\n' % tc)

td = timeit.timeit('list_del_d(N_MAX)', globals=g, number=NUMBER)
print('%f seconds  for i in range(len(v)-1, -1, -1): del v[i]\n' % td)

te = timeit.timeit('list_del_e(N_MAX)', globals=g, number=NUMBER)
print('%f seconds  for i in reversed(range(len(v))): v.pop(i)\n' % te)

tf = timeit.timeit('list_del_e(N_MAX)', globals=g, number=NUMBER)
print('%f seconds  for i in range(len(v)-1, -1, -1): v.pop(i)\n' % tf)

測定結果

一番上が公式マニュアルに載っていた『一般的な削除方法』です。

10万個の要素を削除するのに要した時間は、1.67秒でした。

『高速な削除方法』のグループは、ほぼ一瞬で完了しています。

スポンサーリンク

中でも、『del文で要素を削除するのが最速』のようです。

インデックスの生成方法でdel文は3種類試しましたが、速さはほぼ同じでした。

計測を実行するごとに、3つのdel文のうち、どれかが1番になっていました。

1.670541 seconds  for t in v[:]: v.remove(t)

0.008083 seconds  for i in reversed(range(len(v))): del v[i]

0.007918 seconds  for i in range(len(v))[::-1]: del v[i]

0.008052 seconds  for i in range(len(v)-1, -1, -1): del v[i]

0.014783 seconds  for i in reversed(range(len(v))): v.pop(i)

0.014620 seconds  for i in range(len(v)-1, -1, -1): v.pop(i)

リストの要素をひと桁減らして、N_MAX=1万個で実行した結果がこちらです。

0.013105 seconds  for t in v[:]: v.remove(t)

0.000796 seconds  for i in reversed(range(len(v))): del v[i]

0.000763 seconds  for i in range(len(v))[::-1]: del v[i]

0.000786 seconds  for i in range(len(v)-1, -1, -1): del v[i]

0.001451 seconds  for i in reversed(range(len(v))): v.pop(i)

0.001456 seconds  for i in range(len(v)-1, -1, -1): v.pop(i)

このくらいの実行時間なら、どの方法でもいいように思います。

一番上の v.remove(t) を使った方法は、リストの要素数で大きく時間が変わっているのが分かりますね。

現実的には、for文で要素を全削除するケースはないと思うので、もっと短時間で完了することが多いと思います。

forループでリストの要素を削除していくメリット

巨大なリストを扱う時に、メモリを節約できるメリットがありました。

1万、2万くらいのリストなら、新しいリストを作って、必要な要素だけ .append() していくのが簡単です。

しかし、これが100万くらいのリストになってくると、新・旧のリストを持つのが苦しくなってきました。

そこで、こういったリストの破壊的な操作が役に立ったんですね。

(プログラミングの参考になれば幸いです)

タイトルとURLをコピーしました