Python和Ruby中each循環(huán)引用變量問題(一個(gè)隱秘BUG?)
雖然這個(gè)問題我是在 Python 里遇到的,但是用 Ruby 解釋起來(lái)比較容易一些。在 Ruby 里,遍歷一個(gè)數(shù)組可以有很多種方法,最常用的兩種無(wú)非是 for 和 each:
arr = ['a', 'b', 'c']
arr.each { |e|
puts e
}
for e in arr
puts e
end
通常我比較喜歡后者,似乎因?yàn)閷懫饋?lái)比較好看,不過(guò)從效率上來(lái)說(shuō)前者應(yīng)該會(huì)稍微快一點(diǎn),因?yàn)楹笳邔?shí)際上是在遍歷的過(guò)程中對(duì)每個(gè)元素都調(diào)用一個(gè) lambda 函數(shù)來(lái)做的,雖然一般情況下并不明顯,不過(guò)設(shè)置上下文并調(diào)用函數(shù)確實(shí)是有開銷的,特別是在動(dòng)態(tài)語(yǔ)言里面(不考慮 JIT 內(nèi)聯(lián)優(yōu)化的話)。不過(guò)這次的問題并不是性能。然而確實(shí)跟“ each 對(duì)每個(gè)元素都會(huì)新建一個(gè) scope 而 for 則不是”有關(guān)。
看下面一段代碼:
arr = ['a', 'b', 'c']
h1 = Hash.new
h2 = Hash.new
arr.each { |e|
h1[e] = lambda { e+'!'}
}
for e in arr
h2[e] = lambda { e+'!' }
end
h1['a'].call # => ?
h2['a'].call # => ?
兩個(gè) call 分別會(huì)得到什么?應(yīng)該已經(jīng)猜到了吧?分別是 'a!' 和 'c!' ,后者之所以是 'c!' 是因?yàn)?for 并沒有在循環(huán)的每一步都重新創(chuàng)建一個(gè) scope ,因此三個(gè) lambda 的 closure 引用到了同一個(gè)變量,而這個(gè)變量在最后一次被賦值為 'c' ,所以導(dǎo)致了這樣的后果。
問題其實(shí)出自我在用 Python 寫的一個(gè)小程序中的一段,代碼類似于這樣:
for prop in public_props:
setattr(proxy, 'get_%s'%prop, lambda: self.get_prop(prop))
其中 proxy 是我提供的一個(gè)代理對(duì)象,將 self 的一些公開的屬性給暴露出去,因?yàn)橐拗茖?duì)非 public 的屬性的訪問,我并不想在這個(gè) proxy 中存放任何到 self 的引用,否則在沒有訪問權(quán)限限制的 Python 里通過(guò)類似 proxy._orig_self.some_private_prop 的方式來(lái)訪問是輕而易舉的。所以最后選擇了上面那樣的做法。
不幸的是,由于像剛才所說(shuō)的那樣,for 并沒有每次都單獨(dú)創(chuàng)建 scope ,因此 closure 全部引用到了同一個(gè)變量上,導(dǎo)致所有的屬性值取出來(lái)都是最后一個(gè)屬性了。看到這樣詭異的 bug ,如果是在 C/C++ 里面,我肯定要懷疑是內(nèi)存或者指針的問題了。不過(guò)想了半天才終于恍然大悟!不過(guò) Python 里面沒有 Ruby 那么方便的 each 可以用,lambda 用起來(lái)也很雞肋,所以最后通過(guò)定義一個(gè)局部的函數(shù)來(lái)解決了:
def proxy_prop(name):
setattr(proxy, 'get_%s'%prop, lambda: self.get_prop(name)
for prop in public_props:
proxy_prop(prop)
最后,還要多嘴一句,對(duì)于之前 Ruby 那個(gè)例子,如果把 each 和 for 的執(zhí)行順序顛倒過(guò)來(lái),會(huì)得到不同的結(jié)果:
h1 = Hash.new
h2 = Hash.new
for e in arr
h2[e] = lambda { e+'!' }
end
arr.each { |e|
h1[e] = lambda { e+'!'}
}
h1['a'].call # => 'c!'
h2['a'].call # => 'c!'
現(xiàn)在兩個(gè)都是 'c!' 了!這是因?yàn)?Ruby 1.8 的實(shí)現(xiàn)里面 block 的參數(shù)可以對(duì)局部變量或者全局變量之類的任何東西進(jìn)行賦值,而不是通常意義上的一個(gè) lambda 函數(shù)的參數(shù)那么簡(jiǎn)單。由于前面的 for 語(yǔ)句在當(dāng)前作用域創(chuàng)建了一個(gè) e 作為局部變量,因此 each 就直接對(duì)這個(gè)局部變量進(jìn)行賦值了,這樣,每次引用到的又變成了同一個(gè)東西,導(dǎo)致了一個(gè)隱秘的 Bug !
值得慶幸的是,block 的這個(gè)“特性”在 Ruby 1.9 中已經(jīng)被去除了,block 的參數(shù)只能是正常參數(shù),所以就不再存在這樣的問題了。希望 1.9 盡快普及吧!
相關(guān)文章
使用Anaconda創(chuàng)建Pytorch虛擬環(huán)境的排坑詳細(xì)教程
PyTorch是一個(gè)開源的Python機(jī)器學(xué)習(xí)庫(kù),基于Torch,用于自然語(yǔ)言處理等應(yīng)用程序,下面這篇文章主要給大家介紹了關(guān)于使用Anaconda創(chuàng)建Pytorch虛擬環(huán)境的相關(guān)資料,需要的朋友可以參考下2022-12-12Python實(shí)現(xiàn)備份MySQL數(shù)據(jù)庫(kù)的方法示例
這篇文章主要介紹了Python實(shí)現(xiàn)備份MySQL數(shù)據(jù)庫(kù)的方法,涉及Python針對(duì)mysql數(shù)據(jù)庫(kù)的連接及基于mysqldump命令操作數(shù)據(jù)庫(kù)備份的相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-01-01解決python2 繪圖title,xlabel,ylabel出現(xiàn)中文亂碼的問題
今天小編就為大家分享一篇解決python2 繪圖title,xlabel,ylabel出現(xiàn)中文亂碼的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-01-01在 Windows 下搭建高效的 django 開發(fā)環(huán)境的詳細(xì)教程
這篇文章主要介紹了如何在 Windows 下搭建高效的 django 開發(fā)環(huán)境,本文通過(guò)一篇詳細(xì)教程實(shí)例代碼相結(jié)合給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07python按照多個(gè)字符對(duì)字符串進(jìn)行分割的方法
這篇文章主要介紹了python按照多個(gè)字符對(duì)字符串進(jìn)行分割的方法,涉及Python中正則表達(dá)式匹配的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-03-03