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





◯ 問題


リスト [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. 処理を適用する


f:id:domodomodomo:20180903171739j:plain


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

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]
for i, e in enumerate(lst):
    lst[i] = e * e

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



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


f:id:domodomodomo:20180903171811j:plain


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

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


純粋に for 文で書くなら次のようになります。 詳細は、後述します。先頭から削除すると、リストの順番が崩れるので reversed を使います。

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

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]

lst
# [4, 2, 6]
3. 1 と 2 の合わせ技

if 文の中で使った結果をリストの要素として使いたい時があります。個人的には可読性がいいので、このままでも全く問題ないと思っていますが...

new_lst = [f(x) for x in lst if f(x) % 2 == 0]


もし関数 f を2回実行していて効率が悪いと思っていた場合は map を使うと便利です。

new_lst = [y for y in map(f, lst) if y % 2 == 0]

Python の map, filter, reduce ってなに?


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

1. 読みやすさ

理由は、簡潔に書けるからです。また、速かったりもします。 リストから条件をつけて要素を削除する方法は、色々な書き方がありますが、どれも煩雑だったりします。

# 1. while 文 ... オススメしない, 面倒だから
# 必要な知識量は少ないので、一番簡単な書き方
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
lst, tmp = [], lst
while tmp:
    e = tmp.pop()
    if e % 2 == 0:
        lst.insert(0, e)
# 2. 高速化した while 文の書き方
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
lst, tmp = [], lst
while tmp:
    e = tmp.pop()
    if e % 2 == 0:
        lst.append(e)
else:
    lst.reverse()
# 3. 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]
# 4. リスト内包表記 ... オススメ
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = [e for e in lst if e % 2 == 0]
# 5. filter クラス ... オススメしない, Guido が嫌いだから
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
lst = list(filter(lambda e: e % 2 == 0, e))


1, 2 のやり方は Python を習いたての人の練習にはいいかもしれません。pop と insert を知っていればいいので。

3 のやり方は Python を習いたての人の練習にはいいかもしれません。enumerate を reversed する方法を考えましょう。 という問題設定から初めて行けばいいかもしれません。

5 のやり方は もし lambda 式を使わなくて済むなら def で既に定義された関数を使うような場合は、良いかもしれません。

2. 速さ

リスト内包表記が、一番速いです。

$ python 4_2_list_in_reverse.py 
#
while_statement_insert                   :  0.0000
while_statement_append                   :  0.0000
for_statement                            :  0.0000
for_statement_list_comprehension         :  0.0000
filter_function                          :  0.0000
#
while_statement_insert                   :  0.0000
while_statement_append                   :  0.0000
for_statement                            :  0.0000
for_statement_list_comprehension         :  0.0000
filter_function                          :  0.0000
#
while_statement_insert                   :  0.0003
while_statement_append                   :  0.0003
for_statement                            :  0.0002
for_statement_list_comprehension         :  0.0001
filter_function                          :  0.0002
#
while_statement_insert                   :  0.0032
while_statement_append                   :  0.0026
for_statement                            :  0.0015
for_statement_list_comprehension         :  0.0009
filter_function                          :  0.0019
#
while_statement_insert                   :  0.0970
while_statement_append                   :  0.0240
for_statement                            :  0.0462
for_statement_list_comprehension         :  0.0083
filter_function                          :  0.0177
$

4_2_list_deletion.py


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











(後編)書き方の詳細

なぜ while 文, insert が遅いのか、del 文と pop メソッドの違い、for ループの中で削除するとなぜ上手くいかないのか、などについて説明します。

1. while 文

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

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

2. while 文(高速化)

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

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


実際に速度比較した結果を見てみると、要素数を大きくするにつれ while 文と insert は悲劇的な遅さになっていきます。

◯ なんで?(適当に読み飛ばしてください。)

どうしてそうなってるのでしょうか?ちゃんと確認してはいないのですが、ざっくり CPython のコードを眺めて見たいと思います。 insert メソッドが呼ばれると PyList_Insert が呼ばれます。 さらに inst1 関数が呼ばれています。

int
PyList_Insert(PyObject *op, Py_ssize_t where, PyObject *newitem)
{

    ... 省略 ...

    return ins1((PyListObject *)op, where, newitem);
}


inst1 を覗いて見ます。すると for (i = n; --i >= where; ) items[i+1] = items[i]; として、配列をひとつずつずらしているのが見えます。 目的の箱を空けるために O(n) の操作が必要になってしまっているのです。 Python のリストは、実際には C の配列だった、と言うわけです。
今更聞けない配列とリストのデータ構造 - Qiita

static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
    
    ... かなり、省略 ...
    
    items = self->ob_item;
    for (i = n; --i >= where; )
        items[i+1] = items[i];
    Py_INCREF(v);
    items[where] = v;
    return 0;
}

Python のソースを読んでみる - Qiita

2. for 文

◯ while 文より遅いのはなんで?

"Python を高速化したい" で詳細は書かせていただいたのですが、Python では大抵の場合、while 文よりも for 文が速いのです。 今回は while 文, append メソッドの方が速い結果となりました。何故でしょうか?

今回の for 文は index を指定して del しています。 恐らくその度に、while 文で insert(0, value) をした時と同じように リスト、CPython で言うところの配列を1つずつずらす処理が 走っているからだと思われます。

◯ for 文による実装への道

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

1. 間違い その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. 間違い その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])
3. 正しいコード

やっと、たどり着きました。前から削除すると順番がずれるので 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] が該当します。

class Person(object):
    def __init__(self, name):
        self.name = name

a = b = Person('Tom')
a is b
del a  # 変数 a を削除すると
a # NameError, 変数 a は消えるけど
b # オブジェクトそのものは消えない
  • del ... 名前を削除する

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


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

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

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

# 1. リスト内包表記
lst = [3, 4 ,1, 2, 5, 6]
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]
lst = list(filter(lambda e: e % 2, lst))


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