不要用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的小實驗〉。

avatar-img
15會員
129內容數
寫點東西自娛娛人
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
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
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
今天早上體育系的同學們去準備體育表演會的彩排,只有研究所的同學去上瑜珈,但是算一算人也不少,有8位呢!因為有臨時加選的新同學,老師讓我們再複習先前教過的嬰兒式->四足跪姿->下犬式->鱷魚式,並一一檢視我們的動作,原本期待只有8個人上課,可以獲得一對一的詳細指導,結果老師好像覺得我狀況還可以,只有給
人生中, 有許多的難以預測, 無法預測的休止符; 無法預測的戲幕間; 無法預測的下一秒, 預測已經是不定數, 何不用已知的,去珍惜現有的? 人心擁有一個屬於過往的暫存體, 正常情況下,在其外圍帶上ON/OFF, 讓人自由開啟與關上, 但也可能因為經常的使用下, 造成故障,無法關上,保持了常開, 成了
Thumbnail
現在的社會環境早已經和以前完全不一樣,而所謂的管教子女也是在近幾年才被重視,在早期我想可能沒有多少父母會在意所謂的教養子女這個問題,因為光生存就已經是問題,當時每個家庭所生養的小孩也多,所以教養這問題並沒有怎麼重視。
Thumbnail
生活中,我們周圍常常都有這類型的人,喜歡用自己所謂的“常規”的邏輯來揣測別人的心理,以自己為中心,以自己的認知,嘗試去揣測別人的心理,人抓摸別人的内心世界。但是,你們知道嗎?適當揣測是可以的,這樣可以做到“善解人意”,過度揣測,但是跟別人心中所想的不一樣的時候,你還據理力爭,就會引起別人的厭惡。
Thumbnail
為何總是一遍又一遍的被放鴿子,雖然是小事一件,但勾起的回憶卻是一大串一長串,是我人太好,太把別人的事或話當一回事,還是是那些人的錯,是我太認真, 還是他們注重的點跟我不一樣,不對唷!我為何要替他們找理由跟藉口,明明就是他們辜負了我。 這種辜負可大可小,是否可以在這些小細節上,或者說是在關鍵時刻,在
Thumbnail
用或不用,舉凡二選一的問題通常選哪邊都是錯的。 到底要不要服藥?重要的是過程,這個家(孩子與父母)是怎麼去想的? 教育照顧者如何獲取知識,解析人性心理歷程,再整理這些資訊從中思考,關於自己所面對的處境應該怎麼辦才好。教育和思考是唯一彌平家庭心理條件差距的方式,提升照顧者的知能和行動。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
今天早上體育系的同學們去準備體育表演會的彩排,只有研究所的同學去上瑜珈,但是算一算人也不少,有8位呢!因為有臨時加選的新同學,老師讓我們再複習先前教過的嬰兒式->四足跪姿->下犬式->鱷魚式,並一一檢視我們的動作,原本期待只有8個人上課,可以獲得一對一的詳細指導,結果老師好像覺得我狀況還可以,只有給
人生中, 有許多的難以預測, 無法預測的休止符; 無法預測的戲幕間; 無法預測的下一秒, 預測已經是不定數, 何不用已知的,去珍惜現有的? 人心擁有一個屬於過往的暫存體, 正常情況下,在其外圍帶上ON/OFF, 讓人自由開啟與關上, 但也可能因為經常的使用下, 造成故障,無法關上,保持了常開, 成了
Thumbnail
現在的社會環境早已經和以前完全不一樣,而所謂的管教子女也是在近幾年才被重視,在早期我想可能沒有多少父母會在意所謂的教養子女這個問題,因為光生存就已經是問題,當時每個家庭所生養的小孩也多,所以教養這問題並沒有怎麼重視。
Thumbnail
生活中,我們周圍常常都有這類型的人,喜歡用自己所謂的“常規”的邏輯來揣測別人的心理,以自己為中心,以自己的認知,嘗試去揣測別人的心理,人抓摸別人的内心世界。但是,你們知道嗎?適當揣測是可以的,這樣可以做到“善解人意”,過度揣測,但是跟別人心中所想的不一樣的時候,你還據理力爭,就會引起別人的厭惡。
Thumbnail
為何總是一遍又一遍的被放鴿子,雖然是小事一件,但勾起的回憶卻是一大串一長串,是我人太好,太把別人的事或話當一回事,還是是那些人的錯,是我太認真, 還是他們注重的點跟我不一樣,不對唷!我為何要替他們找理由跟藉口,明明就是他們辜負了我。 這種辜負可大可小,是否可以在這些小細節上,或者說是在關鍵時刻,在
Thumbnail
用或不用,舉凡二選一的問題通常選哪邊都是錯的。 到底要不要服藥?重要的是過程,這個家(孩子與父母)是怎麼去想的? 教育照顧者如何獲取知識,解析人性心理歷程,再整理這些資訊從中思考,關於自己所面對的處境應該怎麼辦才好。教育和思考是唯一彌平家庭心理條件差距的方式,提升照顧者的知能和行動。