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

閱讀時間約 6 分鐘

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

15會員
128內容數
寫點東西自娛娛人
留言0
查看全部
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
用matplotlib畫正弦函數sin的圖形會有多難呢?應該是挺簡單的。不過,要畫得漂亮讓人滿意,還是非得費一番功夫調整不可。
MOUSEBUTTONDOWN的個性和mouse.get_pressed()是完全相反的。mouse.get_pressed()只活在當下,完全不管過去發生過什麼事;MOUSEBUTTONDOWN則會記得所有還沒處理完的事,只要事件佇列還沒被塞滿的話。
身為頂級藝術細胞缺乏者,總是非常羨慕那些能隨手畫出漂亮圖畫的人。看著自己寫的程式在螢幕上展現出事先無法預期的漂亮圖案,還真是很有成就感,也不免有些興奮地想著:「嘿!看來頂級藝術細胞缺乏者有藥醫了!」 這個圖
忽然覺得,似乎、彷彿、好像、應該有個for... else語法,可以讓程式漂亮一點。
就這樣,因為拖延症犯了想偷懶,結果是對dictionary的使用時機和使用方式,有了新的體會。原來,dictionary這樣用,也是可以的啦!
「蛤?!居然當機!」瞪著畫面凍結的螢幕,心裡一面嘀嘀咕咕,一面敲著鍵盤,企圖死馬當活馬醫,看看能不能免去重開機的麻煩。 一切的努力都是徒然,這是徹底的當機!滑鼠、鍵盤完全失去作用,只餘關電源強迫關機一條路可走。 在重開機的當兒,一面看著螢幕有沒有顯示異常的訊息,一面開始分析可能的當機原因。
用matplotlib畫正弦函數sin的圖形會有多難呢?應該是挺簡單的。不過,要畫得漂亮讓人滿意,還是非得費一番功夫調整不可。
MOUSEBUTTONDOWN的個性和mouse.get_pressed()是完全相反的。mouse.get_pressed()只活在當下,完全不管過去發生過什麼事;MOUSEBUTTONDOWN則會記得所有還沒處理完的事,只要事件佇列還沒被塞滿的話。
身為頂級藝術細胞缺乏者,總是非常羨慕那些能隨手畫出漂亮圖畫的人。看著自己寫的程式在螢幕上展現出事先無法預期的漂亮圖案,還真是很有成就感,也不免有些興奮地想著:「嘿!看來頂級藝術細胞缺乏者有藥醫了!」 這個圖
忽然覺得,似乎、彷彿、好像、應該有個for... else語法,可以讓程式漂亮一點。
就這樣,因為拖延症犯了想偷懶,結果是對dictionary的使用時機和使用方式,有了新的體會。原來,dictionary這樣用,也是可以的啦!
「蛤?!居然當機!」瞪著畫面凍結的螢幕,心裡一面嘀嘀咕咕,一面敲著鍵盤,企圖死馬當活馬醫,看看能不能免去重開機的麻煩。 一切的努力都是徒然,這是徹底的當機!滑鼠、鍵盤完全失去作用,只餘關電源強迫關機一條路可走。 在重開機的當兒,一面看著螢幕有沒有顯示異常的訊息,一面開始分析可能的當機原因。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
今天早上體育系的同學們去準備體育表演會的彩排,只有研究所的同學去上瑜珈,但是算一算人也不少,有8位呢!因為有臨時加選的新同學,老師讓我們再複習先前教過的嬰兒式->四足跪姿->下犬式->鱷魚式,並一一檢視我們的動作,原本期待只有8個人上課,可以獲得一對一的詳細指導,結果老師好像覺得我狀況還可以,只有給
人生中, 有許多的難以預測, 無法預測的休止符; 無法預測的戲幕間; 無法預測的下一秒, 預測已經是不定數, 何不用已知的,去珍惜現有的? 人心擁有一個屬於過往的暫存體, 正常情況下,在其外圍帶上ON/OFF, 讓人自由開啟與關上, 但也可能因為經常的使用下, 造成故障,無法關上,保持了常開, 成了
Thumbnail
現在的社會環境早已經和以前完全不一樣,而所謂的管教子女也是在近幾年才被重視,在早期我想可能沒有多少父母會在意所謂的教養子女這個問題,因為光生存就已經是問題,當時每個家庭所生養的小孩也多,所以教養這問題並沒有怎麼重視。
Thumbnail
生活中,我們周圍常常都有這類型的人,喜歡用自己所謂的“常規”的邏輯來揣測別人的心理,以自己為中心,以自己的認知,嘗試去揣測別人的心理,人抓摸別人的内心世界。但是,你們知道嗎?適當揣測是可以的,這樣可以做到“善解人意”,過度揣測,但是跟別人心中所想的不一樣的時候,你還據理力爭,就會引起別人的厭惡。
Thumbnail
為何總是一遍又一遍的被放鴿子,雖然是小事一件,但勾起的回憶卻是一大串一長串,是我人太好,太把別人的事或話當一回事,還是是那些人的錯,是我太認真, 還是他們注重的點跟我不一樣,不對唷!我為何要替他們找理由跟藉口,明明就是他們辜負了我。 這種辜負可大可小,是否可以在這些小細節上,或者說是在關鍵時刻,在
Thumbnail
用或不用,舉凡二選一的問題通常選哪邊都是錯的。 到底要不要服藥?重要的是過程,這個家(孩子與父母)是怎麼去想的? 教育照顧者如何獲取知識,解析人性心理歷程,再整理這些資訊從中思考,關於自己所面對的處境應該怎麼辦才好。教育和思考是唯一彌平家庭心理條件差距的方式,提升照顧者的知能和行動。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
今天早上體育系的同學們去準備體育表演會的彩排,只有研究所的同學去上瑜珈,但是算一算人也不少,有8位呢!因為有臨時加選的新同學,老師讓我們再複習先前教過的嬰兒式->四足跪姿->下犬式->鱷魚式,並一一檢視我們的動作,原本期待只有8個人上課,可以獲得一對一的詳細指導,結果老師好像覺得我狀況還可以,只有給
人生中, 有許多的難以預測, 無法預測的休止符; 無法預測的戲幕間; 無法預測的下一秒, 預測已經是不定數, 何不用已知的,去珍惜現有的? 人心擁有一個屬於過往的暫存體, 正常情況下,在其外圍帶上ON/OFF, 讓人自由開啟與關上, 但也可能因為經常的使用下, 造成故障,無法關上,保持了常開, 成了
Thumbnail
現在的社會環境早已經和以前完全不一樣,而所謂的管教子女也是在近幾年才被重視,在早期我想可能沒有多少父母會在意所謂的教養子女這個問題,因為光生存就已經是問題,當時每個家庭所生養的小孩也多,所以教養這問題並沒有怎麼重視。
Thumbnail
生活中,我們周圍常常都有這類型的人,喜歡用自己所謂的“常規”的邏輯來揣測別人的心理,以自己為中心,以自己的認知,嘗試去揣測別人的心理,人抓摸別人的内心世界。但是,你們知道嗎?適當揣測是可以的,這樣可以做到“善解人意”,過度揣測,但是跟別人心中所想的不一樣的時候,你還據理力爭,就會引起別人的厭惡。
Thumbnail
為何總是一遍又一遍的被放鴿子,雖然是小事一件,但勾起的回憶卻是一大串一長串,是我人太好,太把別人的事或話當一回事,還是是那些人的錯,是我太認真, 還是他們注重的點跟我不一樣,不對唷!我為何要替他們找理由跟藉口,明明就是他們辜負了我。 這種辜負可大可小,是否可以在這些小細節上,或者說是在關鍵時刻,在
Thumbnail
用或不用,舉凡二選一的問題通常選哪邊都是錯的。 到底要不要服藥?重要的是過程,這個家(孩子與父母)是怎麼去想的? 教育照顧者如何獲取知識,解析人性心理歷程,再整理這些資訊從中思考,關於自己所面對的處境應該怎麼辦才好。教育和思考是唯一彌平家庭心理條件差距的方式,提升照顧者的知能和行動。