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() で削除していく方法でした。
普通、リストの要素を削除するときは、こちらの方法でいいと思います。
リストの要素を削除するコード例(スライスとリムーブ)
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 文を実行する時にリストの浅いコピーを作るので、メモリの消費が少し増えました。
そこで、大きなリストで要素を削除するときは、次の『高速な削除方法』が役に立ちました。
高速な削除方法
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万くらいのリストになってくると、新・旧のリストを持つのが苦しくなってきました。
そこで、こういったリストの破壊的な操作が役に立ったんですね。
(プログラミングの参考になれば幸いです)