【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をコピーしました