在Python 3中緩存Exception對象會造成什么后果?
??Python 3有一個不太容易被注意到的改進:異常對象現(xiàn)在有了一個新的屬性__traceback__。這個屬性自動保存了traceback列表,當每次這個異常被重新raise出來的時候,會自動在__traceback__中追加一條記錄。這個功能對于異步編程來說非常有幫助:在另一個線程或者協(xié)程中拋出的異常,被捕獲、傳輸?shù)狡渌胤?,再重新拋出來的時候,不僅最初的traceback得以保留,每次被重新拋出的記錄也都會保留下來,這樣異常的traceback就可以提供很細致的信息。
說完了好處,再來說這個新功能導(dǎo)致的問題:exception對象現(xiàn)在是一個可變的對象了,每次raise都會修改這個對象。如果將一個exception對象拋出多次,就會保留每次拋出的traceback,可能導(dǎo)致的結(jié)果包括:錯誤的堆棧信息;意外地破壞了運行數(shù)據(jù);內(nèi)存泄漏等等。
舉例:
堆棧信息錯誤很好理解,舉一個意外破壞運行數(shù)據(jù)的例子:
import asyncio
import unittest
async def it(raise_, ignore=True):
yield 1
if not ignore:
await asyncio.sleep(0.01)
try:
raise ValueError('test2') from raise_
except Exception:
import traceback
traceback.print_exc()
if ignore:
pass
else:
raise
await asyncio.sleep(0.1)
yield 2
class MyTest(unittest.TestCase):
def test(self):
try:
raise ValueError('testerror')
except ValueError as e:
exc = e
async def task1():
with self.assertRaises(ValueError):
async for i in it(exc, False):
print("task1: ", i)
async def task2():
async for i in it(exc, True):
print("task2: ", i)
async def main():
t1 = asyncio.ensure_future(task1())
t2 = asyncio.ensure_future(task2())
await t1
await t2
asyncio.get_event_loop().run_until_complete(main())
if __name__ == '__main__':
unittest.main()
運行這段代碼,你會很驚訝地發(fā)現(xiàn)本來應(yīng)該很快結(jié)束的程序居然卡住了。原因在于assertRaises這個unittest庫中的函數(shù),為了在保存單元測試結(jié)果的過程中不要占用太多內(nèi)存,所以在保存異常時強制清空了異常堆棧中的locals變量。然而因為這個異常同時在兩個Task中被使用,assertRaises捕獲到的異常堆棧中,還有尚未退出的協(xié)程的堆棧,清空了這個協(xié)程的堆棧會導(dǎo)致這個協(xié)程沒有辦法繼續(xù)正常執(zhí)行下去,進一步導(dǎo)致相應(yīng)的Future沒有人設(shè)置,等待這個Future的過程就無法正常結(jié)束了。
再舉一個內(nèi)存泄漏的例子:這個例子不在用戶代碼里,而在PyPy3 6.0版本的解釋器里。在PyPy3中,為了提高運行速度,關(guān)閉一個生成器使用的GeneratorExit對象被解釋器緩存了起來,這導(dǎo)致每次調(diào)用生成器的close方法時,當前close的生成器的frame都被保存到了這個global對象里,導(dǎo)致了生成器對象和frame對象都無法被GC回收,產(chǎn)生了嚴重的內(nèi)存泄漏。
下面這段代碼在PyPy 3中會迅速耗盡系統(tǒng)內(nèi)存,而在CPython 3中則沒有問題:
def test():
yield 1
while True:
t = test()
t.close()
結(jié)論:
在Python 3中,最佳實踐是:
- 永遠不要持久保存一個已經(jīng)被拋出過的
Exception對象 - 每個被捕獲的
Exception對象,至多被重新raise一次(不管經(jīng)過怎樣的過程) - 在需要將同一個異常廣播到多個過程時(例如:多個過程等待了同一個異步過程),最好每次都重新復(fù)制整個
Exception對象,或者為每個過程創(chuàng)建一個新的Exception對象。通過Python 3的新語法raise ... from ...,可以在新的Exception中保留老的traceback。
第三點的一個特例是asyncio中的Future對象,如果一個Future被await了多次,而這個Future拋出了異常,就會出現(xiàn)第三種情形,此時堆棧信息可能會混亂。
到此這篇關(guān)于在Python 3中緩存Exception對象會造成什么后果?的文章就介紹到這了,更多相關(guān)不要在Python 3中緩存Exception對象內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python爬取股票信息,并可視化數(shù)據(jù)的示例
這篇文章主要介紹了Python爬取股票信息,并可視化數(shù)據(jù)的示例,幫助大家更好的理解和使用python爬蟲,感興趣的朋友可以了解下2020-09-09
Python通過調(diào)用有道翻譯api實現(xiàn)翻譯功能示例
這篇文章主要介紹了Python通過調(diào)用有道翻譯api實現(xiàn)翻譯功能,結(jié)合實例形式分析了基于Python實現(xiàn)的有道翻譯api調(diào)用相關(guān)操作技巧,需要的朋友可以參考下2018-07-07

