2023-12-20|閱讀時間 ‧ 約 6 分鐘

不要用for迴圈一面走訪一面移除list的元素

在寫《The Nature of Code閱讀心得筆記——使用Python實作》《The Nature of Code閱讀心得與Python實作》第4.3節時,原書提到,在使用Java的ArrayList時,如果用迴圈一面走訪一面又移除其中的元素,那會有難以察覺的問題存在。寫個小程式測試的結果發現,Python的list也會有一樣的問題。如果不是原書有特別提到,還真的不會察覺這個問題,因為程式跑起來完全沒有異狀,看不出來有任何不對勁的地方。

到底是個什麼樣奇特的問題,能讓人渾然不覺呢?看一下要寫的東西就知道為什麼了。

那一章是在談粒子系統(particle system)的模擬,其中一個效果就是粒子會在畫面上不斷冒出來,過了一段時間之後又會自動消失,所以粒子的數量會一直變動。這個用list來寫剛剛好,因為list有append()和remove()這兩個方法,可以新增、移除裡頭的元素。把那些不斷冒出來又消失的粒子放在list裡頭,每次更新畫面時,利用append()把新的粒子加到list裡頭,並利用for迴圈來走訪list裡頭所有的粒子,當發現該被移除的粒子時,就把那個粒子用remove()移除。

挺容易的,不是嗎?不過壞就壞在那個移除粒子的部分,如果沒注意到一個小細節,那每次更新畫面用for迴圈走訪list中的所有元素時,可能會有幾個漏掉沒走訪到,因而沒有更新那幾個粒子的狀態。因為畫面是一直在不斷更新的,這次漏掉沒更新狀態的粒子,下次可能就不會被漏掉,再加上畫面中一大堆的粒子,即便有幾個粒子的狀態沒有更新,還真的是很難察覺。那為什麼會這樣呢?用個簡單的例子來看,就會很清楚了。

下面這段程式,最後會印出什麼呢?是[4, 5]嗎?

a = [1, 2, 3, 4, 5]
for i in a:
if i<=3:
a.remove(i)

print(a)

這段程式的目的,是把1、2、3三個元素從a裡頭移除。不過,最後印出來的答案卻是[2, 4, 5],也就是說,漏掉2沒移除。為什麼?!到底發生了什麼事?

一開始的時候,也就是當我們位於a[0]時,因為a[0]的值是1,所以我們把1這個元素從a中移除。這時候,a會變成[2, 3, 4, 5],而我們還是在a[0]這個位置。

既然a[0]已經處理過了,不管實際上a變成什麼樣子,反正接下來當然就是要處理a[1],也就是3。因為3<=3,所以把3也移除,a變成[2, 4, 5]。

a[1]處理過了,再來就是處理a[2],也就是5。因為5>3,所以沒有被移除,最後剩下的,就是[2, 4, 5]。

稍微精簡一下這過程:

位置在a[0],這時a=[1, 2, 3, 4, 5]。移除a[0],也就是1,a變成[2, 3, 4, 5],位置a[0]處理完畢。

位置移到a[1],這時a=[2, 3, 4, 5]。移除a[1],也就是3,a變成[2, 4, 5],位置a[1]處理完畢。

位置移到a[2],這時a=[2, 4, 5]。a[2],也就是5,不用移除,a不變,還是[2, 4, 5],位置a[2]處理完畢。

a[2]已經處理完了,接下來應該處理a[3],但現在a=[2, 4, 5],沒有a[3],所以工作結束,for迴圈執行完畢。

從上面的分析可以知道,利用for迴圈來逐個走訪處理list的元素時,就只會一個位置一個位置依序地走訪處理過去,一個位置就只處理一次,不管已經處理過的那個位置中的內容,是不是有變動,絕不回頭處理第二次。這就是前面提到的,會造成有些粒子的狀態沒有被更新的原因。

造成問題的原因知道了,那要怎麼解決呢?解決的辦法,其實非常簡單,看是要用相反的順序來處理list的元素,或者是建立一個list的副本,然後檢查副本的元素而移除正本的元素,都可以。

用相反的順序來處理list的元素,也就是本來是由左至右依序處理,現在改成由右至左來處理,程式可以這樣寫:

for i in range(len(a)-1,-1,-1):
if a[i] <= 3:
a.remove(a[i])

因為會被移除的,都是已經走訪過的元素,所以也就不會有元素被漏掉沒處理到的問題。不過,這個寫法的缺點是不容易閱讀,而且萬一元素的處理順序不能改變,一定要由左至右處理,那就行不通了。

用建立副本的方式來寫,可以寫成這樣:

for i in a.copy():
if i<=3:
a.remove(i)

這裡的a.copy(),也可以改用a[:]。因為走訪的是副本的元素,而移除的是正本的元素,所以就不會有元素被漏掉沒處理到的問題。這個寫法比較容易閱讀,而且也不用擔心元素處理順序的問題。

解決了for迴圈邊走訪邊移除list元素的問題之後,應該馬上會想到另一個問題:那在for迴圈中,邊走訪邊增加list的元素,這樣會有問題嗎?這問題的答案是:會有問題,發作時的症狀會很明顯、很容易察覺,但是後果會很嚴重。欲知詳情,請看〈一個關於for loop和list的小實驗〉。

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.