在寫《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的小實驗〉。