Last Updated: 2/6/2024, 5:44:57 AM

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

リスト内包表記が、オススメです。

# 1. はじめに

# ◯ 問題


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

# ◯ 解答 その1 新しいリストを作り、要素を追加する。


リスト内包表記が、オススメです
(大事なことなので、2回言いました的な...)




下記の書き方は「リスト内包表記」と呼ばれる書き方です。

# 対話モード >>> にコピペで実行できます。
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = [e for e in lst if e % 2 == 0]
new_lst
# [4, 2, 6]

(補足)
これがもっとも簡単です。 もしメモリが気にならないなら、この方法がオススメです。

ただ、実際には new_lst という新しいリストを作るというズルをしています。 もし、どうしても新しいリスト new_lst を作るのでなく、 純粋に元あるリスト lst から要素を削除しなければならない場合は、以下に書きました。

# ◯ 解答 その2 リストから要素を削除する。

元からあるリストから純粋に要素を削除します。 while 文になります。for 文で書くと「書き方」でも「速度」でもハマります(後述します)。

# 3. while 文(リストから直接削除したい場合)
#     オススメ
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
tmp = []
while lst:
    e = lst.pop()
    if e % 2 == 0:
        tmp.append(e)

while tmp:
    lst.append(tmp.pop())

lst
# [4, 2, 6]

ただ、思いの外、メモリが気になるから、 リストから直接要素を削除しようという重たい状況って無いのかなと思ったりします。 無理せず簡単な方を使うのを個人的にオススメしたいなと思ったり、思わなかったりします。 コードも綺麗になるので。

# 2. リスト内包表記への道

# Step 1. 処理を適用する




リストの各要素に処理を適用したいときが、よく あります。

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]

そんな よく ある書き方なので「リスト内包表記」という書き方が用意されています。 こういう専用の簡単な書き方のことを一般に 糖衣構文 (opens new window) と言います。

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

new_lst = [e * e for e in lst]

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

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


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

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

new_lst = []
for e in lst:
    if e % 2 == 0:
        new_lst.append(e * e)

new_lst
# [4, 2, 6]

これがその書き方です。

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

# 3. メリットとデメリット

# ◯ リスト内包表記のメリット

簡潔で読みやすいです。また、実行速度も速いです。 可読性、実行速度、なぜ速度が違うのかと言った、かなり細かいことは後編に書きました。

# ◯ リスト内包表記のデメリット

メモリを消費します。よくみるとリスト内包表記は新しいリスト new_lst を1つ作ってしまっています。 その分だけメモリを消費してしまいます。

ジェネレータ式 を使うことで回避することができます。 本当の本当は ジェネレータ式 がオススメなのですが、地味に難しいです。 もし使用するメモリの量が気になるようになってきたら、オススメの機能です。 そうでなければ無理して使う必要はないかなと思います。

また、リスト内包表記で for 文の代替はできません。 複雑なことはできないので、もしあまりに複雑になりそうなら、素直に for 文で書いた方が便利です。 複雑なことには使えないと言うのは、糖衣構文全般に言えることです。

# 4. 書き方の比較検討

書き方は大きく分けて2つに分けることができます。 上で見たように新しいリストに要素を条件で分類しつつ追加する方法と(1, 2)、 元からあるリストから要素を削除する方法です(3, 4)。

# 1. for 文
#     オススメしない, 書き方が面倒だから
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = []
for e in lst:
    if e % 2 == 0:
        new_lst.append(e * e)

new_lst
# [4, 2, 6]
# 2. リスト内包表記
#     オススメ
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
new_lst = [e for e in lst if e % 2 == 0]
new_lst
# [4, 2, 6]
# 3. while 文(リストから直接削除したい場合)
#     オススメ
lst = [3, 9, 4 ,1, 2, 5, 7, 6]
tmp = []
while lst:
    e = lst.pop()
    if e % 2 == 0:
        tmp.append(e)

while tmp:
    lst.append(tmp.pop())

lst
# [4, 2, 6]
# 4. for 文(リストから直接削除したい場合)
#     絶対にオススメしません, 遅くなるから
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]

# 5. for 文による実装への道

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

ちなみに for 文で書くと鬼のように重くなるので、  絶対にオススメしません  。 なぜ沼にハマるのかだけ、ざっくり眺めていただけると幸いです。

なぜ遅くなるのかの理由は、このページ末尾にあるリンク先でのページで、 ご説明させていただいております。

# Step 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]
# ちゃんと削除できました。

# Step 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])

# Step 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]

# 6. del 文と pop メソッドの違い

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

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

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

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

a = b = Person('Tom')
a is b
del b  # 変数 b を削除すると
a # オブジェクトそのものは消えないけど
b # NameError, 変数 b は消える
>>> a is b
True
>>>
>>> a
<__main__.Person object at 0x10488b1d0>
>>>
>>> b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined
>>> 


del 文と pop メソッドを使ったので、おさらいがてらに説明させていただきました。




# 7. 悲劇的に遅い for 文

では、なぜ for 文を使うと悲劇的に遅くなるのでしょうか? それは Python のリストが、一般にコンピュータサイエンスで 言われる「リスト」というよりも C 言語の「配列」に近いものだからです。

以下の記事で説明させて位いただきました。 前半のコードは読み飛ばしていただいて、 後半の図入りのところだけ、サクッと眺めていただければ、 雰囲気だけ伝わるかなと思います。

自分も雰囲気でしか理解していませんが笑