Python で list を条件をつけて複数の要素を削除したい。





◯ 問題


リスト [3, 5, 6, 1, 2, 9, 7, 4, 8, 0] から奇数の要素を削除してください。



◯ 解答


結論だけ言えば、リスト内包表記が一番オススメです


lst = [3, 9, 4 ,1, 2, 5, 7, 6]
[e for e in lst if e % 2 == 0]
# [4, 2, 6]


◯ リスト内包表記

1. 処理を適用する


[式 for 要素 in リスト]


リスト内包表記は、主にリストの各要素に、処理を適用したいときに使います。

lst = [3, 9, 4 ,1, 2, 5, 7, 6]

# 1. 数式を書き込んだり
[e * e for e in lst]
# [9, 81, 16, 1, 4, 25, 49, 36]


# 2. 関数を使ったりもできます。
def f(x):
    return x + 2

[f(e) for e in lst]
# 5, 11, 6, 3, 4, 7, 9, 8


純粋に for 文で書くなら次のようになります。

lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = []
for e in lst:
    new_lst.append(e * e)

new_lst
# [9, 81, 16, 1, 4, 25, 49, 36]



2. 必要なものだけ取り出す。


[式 for 要素 in リスト if 条件]


リスト内包表記は、なんだか便利そうですね。もしこれで必要な要素だけ取り出せたら、もっと便利そうです。これがこの書き方です。

lst = [3, 9, 4 ,1, 2, 5, 7, 6]
[e for e in lst if e % 2 == 0]
# [4, 2, 6]


◯ リスト内包表記がおすすめである理由

理由は、簡潔に書けるからです。また、速かったりもします。 そのほか、色々な書き方がありますが、どれも煩雑だったりします。

# 1. while 文 ... オススメしない, 面倒だから
# 必要な知識量は少ないので、一番簡単な書き方
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = []
while lst:
    e = lst.pop()
    if e % 2 == 0:
        new_lst.insert(0, e)
# 2. for 文 ... オススメしない, 面倒だから
# 正しいコード
lst = [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]

def reversed_enumerate(seq):
    return zip(reversed(range(len(lst))), reversed(lst))

for i, e in reversed_enumerate(lst):
    if e % 2 == 1:
        del lst[i]
# 3. リスト内包表記 ... オススメ
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = [e for e in lst if e % 2 == 0]
# 4. filter クラス ... オススメしない, Guido が嫌いだから
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = list(filter(lambda e: e % 2 == 0, e))

1. while 文

空のリスト new_lst を1つ用意して、先頭に insert していきます。

lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = []
while lst:
    e = lst.pop()
    if e % 2 == 0:
        new_lst.insert(0, e)


でも実は、こう書いた方が速かったりします。 それでもリスト内包表記よりは遅いですが。 実は Python ではリストの先頭に insert するのは insert(0, e) は重い処理になります。

lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = []
while lst:
    e = lst.pop()
    if e % 2 == 0:
        new_lst.append(e)
else:
    new_lst.reverse()


実際に速度比較をしました。N を書き換えて、時間がどう変化するか、遊んでみてください。N が大きくなるほど、insert は悲劇的な遅さになります。
append_vs_insert.py

どうしてそうなってるのか?を知りたければ CPython の list クラスのコードを読むといいかもしれません。僕は、一切理解していません。
Python のソースを読んでみる - Qiita

リストの操作の速度比較をした記事です。
あなたのPythonを爆速にする7つの方法

Python のコードは高速化しなくていいかなと思っています。自分が読みやすいと思った書き方で書いてください。その辺の話については、ここで書きました。

2. for 文

while 文ではなく for 文でやろうとすると、少し沼にハマります。新たに del 文, enumerate クラス, reversed クラスの3つを使います。

間違い その1

# 間違いコード
lst = [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]
for e in lst:
    if e % 2 != 0:
        del e

lst
# [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]
# あれ?何も削除できていない (´;ω;`)ブワッ


なぜ、このコードが間違いかと言うと del e が、変数 e を削除しているだけだからです。 例えば次のコードをみてましょう。

# 動作確認コード
lst = [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]
e = lst[3]
e  # 1
del e
e  # NamError

lst
# [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]
# 変数 e を削除しても lst には何の影響もない


では、次のコードを見てみましょう。添字表記で行けば、うまく削除できそうですね。

# 動作確認コード
lst = [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]
del lst[3]

lst
# [3, 5, 6, 2, 9, 7, 4, 8, 0]
# ちゃんと削除できました。

間違い その2

早速書き換えてみました。それでも、間違っています。

# 間違いコード
lst = [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]
for i, e in enumerate(lst):
    if e % 2 != 0:
        del lst[i]

lst
# [5, 6, 2, 7, 4, 8, 0]
# あれ...?(´;ω;`)ブワッ


この原因は del lst[i] するたびにリストが短くなっているからです。しかし i は、リストが短くなっても、ループするたびに1ずつ加算されていきます。

# 確認コード
lst = [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]
for i, e in enumerate(lst):
    i, e, lst
    if e % 2 != 0:
        del lst[i]

# lst は短くなっても i は1ずつ増える。
# (i, e, lst)
# (0, 3, [3, 5, 6, 1, 2, 9, 7, 4, 8, 0])
# (1, 6, [5, 6, 1, 2, 9, 7, 4, 8, 0])
# (2, 1, [5, 6, 1, 2, 9, 7, 4, 8, 0])
# (3, 9, [5, 6, 2, 9, 7, 4, 8, 0])
# (4, 4, [5, 6, 2, 7, 4, 8, 0])
# (5, 8, [5, 6, 2, 7, 4, 8, 0])
# (6, 0, [5, 6, 2, 7, 4, 8, 0])

正しいコード

やっと、たどり着きました。前から削除すると順番がずれるので reversed を使って、後ろから削除します。

# 正しいコード
lst = [3, 5, 6, 1, 2, 9, 7, 4, 8, 0]

def reversed_enumerate(seq):
    return zip(reversed(range(len(lst))), reversed(lst))

for i, e in reversed_enumerate(lst):
    if e % 2 == 1:
        del lst[i]

del 文と pop メソッドの違い

削除する値を使いたいなら pop を使います。削除する値を使わないなら del を使います。

例えば while 文の例では削除した要素を再利用しているので pop を使いました。for 文の例では削除した要素を再利用しないので del を使いました。

del には、オブジェクトを削除すると言うよりも、名前を削除すると言う意味合いが強いです。名前とは変数名 val, 属性名 obj.attr , 添字表記 seq[0] が該当します。

  • del ... 名前を削除する

  • pop ... 要素を取得する


for 文の中で del とか pop をすると、うまく動作しません。その辺りのことについて、ここで書きました。とても長い長いたびに出ることになります。

3. リスト内包表記(これがオススメ)

for 文を書くのは面倒です。Python にはリスト内包表記という書き方があります。 これが一番オススメです。

# 1. リスト内包表記
lst = [3, 4 ,1, 2, 5, 6]
new_lst = [e for e in lst if e % 2 == 0]


1, 2, 4 のやり方は、あまりオススメではありません。 1, 2 は 面倒だから。4 は Guido が filter クラスが嫌いだから。

4. filter クラス

filter クラスは list から条件に適合した要素だけを取り出したリストを生成してくれます。

# 2. filter クラス
lst = [3, 4 ,1, 2, 5, 6]
new_lst = list(filter(lambda e: e % 2, lst))


filter はイテレータです。これを説明するには、とても、とても長い旅に出ることになります。 まず、filter について説明していきます。