人生リアルタイムアタック

当面はPython学習帳

Pythonで破壊的ループをする際はリスト全体のコピーをとる

Pythonで以下のような破壊的ループをしようとすると、indexのズレが発生してすべての要素に対して処理が行われないケースがある。
(この例の処理ではリスト内包表記で充分代替可能だったりするが、あくまで例として単一処理にしている。)

li = [
    {
        "id": "D028xxxxx",
        "is_im": True,
        "user": "USLACKBOT",
        "created": 1397471294,
        "is_user_deleted": False
    },
    {
        "id": "D028xxxxx",
        "is_im": True,
        "user": "U028xxxxx",
        "created": 1397471294,
        "is_user_deleted": False
    },
    {
        "id": "D028xxxxx",
        "is_im": False,
        "user": "U028xxxxx",
        "created": 1397471294,
        "is_user_deleted": False
    }
]

for user in li:
    if user['is_im'] is True:
        li.remove(user)

print(li) # [{'id': 'D028QH1PT', 'created': 1397471294, 'user': 'U028NTG5T', 'is_im': True, 'is_user_deleted': False}, {'id': 'D028QH1PR', 'created': 1397471294, 'user': 'U028P546G', 'is_im': False, 'is_user_deleted': False}]
# 'is_im': Trueの要素が残っている

リスト全体のスライスコピーを取る

正しく破壊的ループをする場合には、リスト全体をコピーして実行すると良い。
ループ処理前に temp_li = li を行うやり方もWebでは散見されたが、
li[:] によるスライスコピーが一番綺麗に記述できる。

for user in li[:]:
    if user['is_im'] is True:
        li.remove(user)

print(li) # [{'created': 1397471294, 'id': 'D028QH1PR', 'is_im': False, 'is_user_deleted': False, 'user': 'U028P546G'}]

filter関数を使う

ちなみにfilter()で書くとこう。

li = list(filter(lambda user: user['is_im'] is False, li))

print(li) # [{'created': 1397471294, 'id': 'D028QH1PR', 'is_im': False, 'is_user_deleted': False, 'user': 'U028P546G'}]

Python3からはfilter()はリストでなくイテレータを返却するようになったので、list()で囲う必要がある。

リスト内包表記を使う

結局はリスト内包表記がいいのかも知れない。

li = [user for user in li if user['is_im'] is False]

print(li) # [{'created': 1397471294, 'id': 'D028QH1PR', 'is_im': False, 'is_user_deleted': False, 'user': 'U028P546G'}]

上記の内容はPythonチュートリアルに記載されていた。数年ぶりに読み返したが、まだ発見があったのでまだまだだなと痛感。

Pythonチュートリアル 第2版

Pythonチュートリアル 第2版

一応速度も計測してみたが、今回のリストだと速度に顕著な差異が見られないので割愛した。 コードはPython3.xに準拠。