一篇文章助力大家理解Python 代碼中的垃圾回收機(jī)制
回復(fù)“書籍”即可獲贈(zèng)Python從入門到進(jìn)階共10本電子書

GNE: 新聞網(wǎng)頁正文通用抽取器[1]更新了0.2.1版本,大幅度提高了正文的提取速度。在開發(fā)這個(gè)版本的時(shí)候,我遇到了一個(gè)非常奇怪的 Bug,最終發(fā)現(xiàn)是由于垃圾回收機(jī)制和內(nèi)存重用機(jī)制導(dǎo)致的。今天我們來看看這個(gè)問題。
問題背景
先來看一段代碼:

這段代碼讀取tests/163/9.html這個(gè)文件里面的 HTML 代碼,分別獲取 下面的所有標(biāo)簽內(nèi)部的所有標(biāo)簽中的文本。說起來可能有點(diǎn)繞口,我舉個(gè)例子。
世界
分別獲取 但這段代碼有個(gè)問題,就是對(duì)于嵌套結(jié)構(gòu)的標(biāo)簽,會(huì)重復(fù)提取。例如: 首先,獲取 這樣一來,在上圖代碼里面第15-20行就會(huì)重復(fù)執(zhí)行兩次。 為了提高代碼的運(yùn)行效率,我們引入緩存,記錄每一個(gè) 于是,代碼修改成下面這樣: 代碼第18行的 這段代碼看起來似乎沒有什么問題,但在實(shí)際提取數(shù)據(jù)的時(shí)候,發(fā)現(xiàn)提取的結(jié)果不太正常。 為了調(diào)試這個(gè)問題,我對(duì)代碼做了一下修改: 可以看到,同一個(gè) HTML 標(biāo)簽,之前緩存的結(jié)果竟然跟新提取的不一樣。 于是,我想看看每次提取的時(shí)候,對(duì)應(yīng)的 element 是哪個(gè),但卻發(fā)生了更詭異的事情,我們做一個(gè)看起來對(duì)代碼不會(huì)有任何影響的改動(dòng): 圖4里面,我們直接把 但奇怪的事情就這樣發(fā)生了,問題消失了!在圖4大量打印的 但為什么會(huì)發(fā)生這種事情呢?難道說跟緩存的結(jié)果有關(guān)系?那么我們把列表里面的 僅僅是把 它似乎知道我在試圖去觀察它,當(dāng)我嘗試用代碼去觀察 遇事不決,量子力學(xué)。這個(gè)問題跟量子力學(xué)實(shí)際上沒有關(guān)系。導(dǎo)致這個(gè)詭異情況發(fā)生的原因,是一個(gè)一直運(yùn)行在 Python 里面,但是你常常忽略的機(jī)制——垃圾回收。 Python 會(huì)把不再使用的對(duì)象清理掉,從而釋放內(nèi)存。當(dāng)我們執(zhí)行一個(gè) for 循環(huán)時(shí): 循環(huán)第一次執(zhí)行的時(shí)候,生成第一個(gè) 但如果換一種寫法: 由于列表 在示例代碼里面,大家注意 一開始,我有一個(gè)不正確的假設(shè),我以為 但實(shí)際上這是不正確的。因?yàn)槿绻耙粋€(gè)節(jié)點(diǎn)的內(nèi)存區(qū)域被垃圾回收了,那么這個(gè)區(qū)域會(huì)被重新分配,新來的節(jié)點(diǎn)可能碰巧會(huì)放到這個(gè)地方,這就導(dǎo)致兩個(gè)不同的 而當(dāng)我使用 所以,bug 的根本原因在于,我不應(yīng)該使用 于是,修改代碼,把 問題得以解決。 GNE: 新聞網(wǎng)頁正文通用抽取器: https://github.com/kingname/GeneralNewsExtractor -------------------?End?------------------- 往期精彩文章推薦: 歡迎大家點(diǎn)贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持 想加入Python學(xué)習(xí)群請(qǐng)?jiān)诤笈_(tái)回復(fù)【入群】 萬水千山總是情,點(diǎn)個(gè)【在看】行不行 /今日留言主題/ 隨便說一兩句吧~~標(biāo)簽下面的標(biāo)簽中的文本,也就是你好和世界。 標(biāo)簽,獲取到的是你好所在的標(biāo)簽。但是,獲取標(biāo)簽下面的標(biāo)簽時(shí),獲取的仍然是同一個(gè)標(biāo)簽。標(biāo)簽的分析結(jié)果,如果發(fā)現(xiàn)一個(gè)標(biāo)簽已經(jīng)被分析了,就直接使用緩存的結(jié)果,避免重復(fù)分析。
str(element)對(duì)應(yīng)了這個(gè)節(jié)點(diǎn)的內(nèi)存地址,如下圖所示:
薛定諤的 Element


element_text_list緩存起來。圖5里面,我們把[element_text_list, element]緩存起來,讀取的時(shí)候,讀取這個(gè)列表的下標(biāo)為0的元素。也就是說,這個(gè)緩存的element我們根本不使用。同一個(gè)標(biāo)簽,緩存的數(shù)據(jù)跟提取的數(shù)據(jù)不一致!,在圖5里面卻一條都沒有打印。這樣修改以后,GNE 的提取的結(jié)果就正確了。element改成其他數(shù)據(jù)看看:
element改成了數(shù)字1,Bug 又出現(xiàn)了。element時(shí),它就一切正常。當(dāng)我不觀察它時(shí),它就會(huì)出問題。薛定諤的 element。看不見的手
for element in element_list: a = element.xpath('//xxx') b = element.xpath('.//text()') c = 1 + 1element對(duì)象,但是這個(gè)對(duì)象在循環(huán)第二次執(zhí)行的時(shí)候就被新的element對(duì)象覆蓋了。因?yàn)闆]有其他地方繼續(xù)使用第一個(gè) element 對(duì)象,它的引用計(jì)數(shù)歸零,Python 的垃圾回收機(jī)制就會(huì)把它清理掉。它占用的內(nèi)存空間也會(huì)被釋放出來。cache = []for element in element_list: a = element.xpath('//xxx') b = element.xpath('.//text()') c = 1 + 1 cache.append(element)cache中包含了對(duì)每個(gè) element 對(duì)象的引用,導(dǎo)致第一次循環(huán)生成的element對(duì)象的引用計(jì)數(shù)不為0,垃圾回收機(jī)制不會(huì)回收它,它始終占用了一塊內(nèi)存區(qū)域。這塊區(qū)域不會(huì)被其他數(shù)據(jù)使用。那么每次循環(huán),新的element對(duì)象都會(huì)新申請(qǐng)一塊內(nèi)存區(qū)域來存放數(shù)據(jù),于是就等價(jià)于每一個(gè)不同的 element 節(jié)點(diǎn)對(duì)應(yīng)了不同的內(nèi)存地址。element_flag = str(element)這一行,它的值類似于,這里的十六進(jìn)制數(shù)字0x1087ba638對(duì)應(yīng)了這個(gè)對(duì)象在內(nèi)存里面的地址。str(element)的值,對(duì)應(yīng)的 HTML 里面的每個(gè)節(jié)點(diǎn)。同一個(gè)節(jié)點(diǎn),多次執(zhí)行,結(jié)果都一樣,不同的節(jié)點(diǎn),多次執(zhí)行,結(jié)果都不一樣。 標(biāo)簽,當(dāng)你執(zhí)行str(element)時(shí),他們打印出來的結(jié)果都是相同的。但是實(shí)際上他們的正文不一樣。element_text_cache[element_flag] = [element_text_list, element]時(shí),由于每個(gè)element對(duì)象不會(huì)被回收,于是就不會(huì)出現(xiàn)不同的節(jié)點(diǎn)互相覆蓋的問題,所以它的工作就符合了預(yù)期。解決問題
str(element)作為緩存的 Key,應(yīng)該找一個(gè)跟 HTML 節(jié)點(diǎn)一一對(duì)應(yīng)的東西來作為 Key。顯然,使用 XPath 更好。element_flag改成 XPath:
參考資料

