python with提前退出遇到的坑與解決方案
問(wèn)題的起源
早些時(shí)候使用with實(shí)現(xiàn)了一版全局進(jìn)程鎖,希望實(shí)現(xiàn)以下效果:
with CacheLock("test_lock", 10): #如果搶占到鎖,那么就執(zhí)行這段代碼 # 否則,讓with提前退出
全局進(jìn)程鎖本身不用多說(shuō),大部分都依靠外部的緩存來(lái)實(shí)現(xiàn)的,redis上用的是setnx,有時(shí)候根據(jù)需要加上緩存擊穿問(wèn)題、隨機(jī)延后以防止對(duì)緩存本身造成壓力
當(dāng)時(shí)同樣寫了單元測(cè)試來(lái)測(cè)試這段代碼的有效性:
with CacheLock("test_lock", 10): value = cache.get("test_lock") self.assertEqual(value, 1) with CacheLock("test_lock", 10): # 不會(huì)進(jìn)到這里 self.assertFalse(True) value = cache.get("test_lock") self.assertEqual(value, None)
看起來(lái)非常完美地通過(guò)了。
這樣的一個(gè)全局進(jìn)程鎖是通過(guò) __enter__ 方法拋出異常, __exit__ 方法中捕獲異常來(lái)實(shí)現(xiàn)的:
class CacheLock(object): def __init__(self, lock_key, lock_timeout): self.lock_key = lock_key self.lock_timeout = lock_timeout self.success = False def __enter__(self): self.success = cache.lock(self.lock_key, self.lock_timeout) if self.success: return self else: raise LockException("not have lock") def __exit__(self, exc_type, exc_value, traceback): #沒(méi)有搶到鎖的時(shí)候,啥都不做? if self.success: await cache.delete(self.lock_key) if isinstance(exc_value, LockException): return True if exc_type: raise exc_value
看起來(lái)還不錯(cuò),畢竟單元測(cè)試都過(guò)了。
但是,這樣的實(shí)現(xiàn)是有問(wèn)題的:
原因在于 __exit__ 的執(zhí)行不是包在 __enter__ 之外的,因此 __enter__ 拋出的異常,不會(huì)被 __exit__ 捕獲。
上面的單元測(cè)試恰好通過(guò),是因?yàn)槠渲杏袃蓚€(gè)with語(yǔ)句,外面的with 捕獲的其實(shí)是里面的 __enter__ 拋出的異常
使用改進(jìn)后的單元測(cè)試:
cache.set("test_lock",1) with CacheLock("test_lock", 10): self.assertFalse(True) value = cache.get("test_lock") self.assertEqual(value, None)
就會(huì)發(fā)現(xiàn)單元測(cè)試過(guò)不去了。
這個(gè)問(wèn)題是我試圖使用with實(shí)現(xiàn)另一個(gè)邏輯:AB測(cè)試 時(shí)出現(xiàn)的,同樣是 __enter__ 拋出異常, __exit__ 試圖捕獲:
import operator class EarlyExit(Exception): pass class ABContext(object): """AB測(cè)試上下文 >>> with ABContext(newVersion, consts.ABEnum.layer2): >>> # dosomething """ def __init__(self, version, ab_layer, relationship="eq"): self.version = version self.ab_layer = ab_layer # 如果不存在這種操作符,那就提前報(bào)錯(cuò) self.relationship = getattr(operator, relationship) def __enter__(self): # 如果不滿足條件,等于不執(zhí)行上下文中的內(nèi)容 if not self.relationship(self.version, self.ab_layer.value): raise EarlyExit("not match") return self def __exit__(self, exc_type, exc_value, traceback): if exc_value is None: return True if isinstance(exc_value, EarlyExit): return True if exc_type: raise exc_value return True
調(diào)試沒(méi)有通過(guò)的單元測(cè)試的時(shí)候發(fā)現(xiàn),拋出異常后根本沒(méi)有執(zhí)行到 __enter__
第一種解決方案
既然想明白了with的執(zhí)行順序,那么第一種解決方案就呼之欲出了:既然__exit__捕獲的異常在__enter__執(zhí)行完成之后,那么我們提供一個(gè)函數(shù)確認(rèn)一下就可以了,把ABContext實(shí)現(xiàn)改成這樣:
def ensure(self): if not self.relationship(self.version, self.ab_layer.value): raise EarlyExit("not match") def __enter__(self): # 如果不滿足條件,等于不執(zhí)行上下文中的內(nèi)容 return self
使用的時(shí)候:
with ABContext(newVersion, consts.ABEnum.layer2) as c: c.ensure() # 執(zhí)行其他的想要執(zhí)行的代碼
但這樣的解決方法并不優(yōu)雅,萬(wàn)一使用這個(gè)ABContext的時(shí)候忘記用ensure方法了,那么就等于完全沒(méi)用這個(gè)Context方法,太容易失誤了,而且代碼也失去了Pythonic的性質(zhì)
第二種解決方法
翻了一下contextlib的標(biāo)準(zhǔn)庫(kù)文檔,發(fā)現(xiàn)有一個(gè)已經(jīng)廢棄的函數(shù): contextlib.nested
from contextlib import nested with nested(*managers): do_something()
可以執(zhí)行多個(gè)上下文.
from contextlib import nested with nested(A(), B(), C()) as (X, Y, Z): do_something() # is equivalent to this: m1, m2, m3 = A(), B(), C() with m1 as X: with m2 as Y: with m3 as Z: do_something()
這個(gè)廢棄的特性在Python2.7之后,可以直接由with關(guān)鍵字執(zhí)行,形如:
with context1,context2: #do something
這個(gè)特性還不錯(cuò),根據(jù) __enter__ 的執(zhí)行順序的話,那么我們可以實(shí)現(xiàn)一個(gè)由第一個(gè) context的__exit__來(lái)捕獲,第二個(gè)context的__enter__來(lái)拋出異常,
如同這樣:
class AlwaySuccessContext(object): def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if isinstance(exc_value, EarlyExit): return True if exc_type: raise exc_value return True
結(jié)合前面我們實(shí)現(xiàn)的ABContext的使用是這樣的:
def test_context_noteq(self): obj = MagicMock(return_value=True) with AlwaySuccessContext(), ABContext(2, const.ABTestEnum.control): self.assertFalse(obj()) obj.assert_not_called()
good,單元測(cè)試就這樣過(guò)了
能不能再給力點(diǎn)?
確實(shí),在with里要寫倆context有點(diǎn)蛋疼,并不是特別優(yōu)雅,能不能還是回到最初的那種用法:我們只用寫一條context,這一個(gè)context做到了兩個(gè)context的事情?
要是nested那個(gè)函數(shù)還在就好了。。要的其實(shí)就是它的功能。
Python3.1之后contextlib提供了一個(gè)ExitStack的功能來(lái)提供一個(gè)模擬的功能,但試了一下發(fā)現(xiàn),實(shí)際上只調(diào)用了__enter__方法,但沒(méi)有做對(duì)應(yīng)的異常捕獲
第三種解決方案
哈哈哈哈把自己繞到圈子里去了,想了一下,同樣是一個(gè)縮進(jìn)的代碼塊,為什么不能用if來(lái)解決呢!不就是個(gè):
def test_context_noteq(self): # 不等的時(shí)候,不會(huì)執(zhí)行with里的內(nèi)容 obj = MagicMock(return_value=True) context = ABContext(2, const.ABTestEnum.control) # print(type(context)) if ABContext(2, const.ABTestEnum.control): self.assertFalse(obj()) obj.assert_not_called()
TIL
總之學(xué)到了contextlib里的一些有用的函數(shù)和裝飾器,也第一次發(fā)現(xiàn)with可以放個(gè)context
雖然放多個(gè)context的動(dòng)態(tài)構(gòu)造還有待研究,with 后面的代碼塊也不能填一個(gè)元組或者列表。。惆悵。。
- python中from module import * 的一個(gè)坑
- Python創(chuàng)建二維數(shù)組實(shí)例(關(guān)于list的一個(gè)小坑)
- Python安裝lz4-0.10.1遇到的坑
- 淺析python中SQLAlchemy排序的一個(gè)坑
- python使用scrapy發(fā)送post請(qǐng)求的坑
- Python編碼爬坑指南(必看)
- python list刪除元素時(shí)要注意的坑點(diǎn)分享
- 詳談python3中用for循環(huán)刪除列表中元素的坑
- 淺談python配置與使用OpenCV踩的一些坑
- Python中一些深不見(jiàn)底的“坑”
相關(guān)文章
Django中Cookie設(shè)置及跨域問(wèn)題處理詳解
本文主要介紹了Django中Cookie設(shè)置及跨域問(wèn)題處理,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09tensorflow基于CNN實(shí)戰(zhàn)mnist手寫識(shí)別(小白必看)
這篇文章主要介紹了tensorflow基于CNN實(shí)戰(zhàn)mnist手寫識(shí)別(小白必看),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07簡(jiǎn)單實(shí)現(xiàn)python收發(fā)郵件功能
這篇文章主要教大家如何簡(jiǎn)單實(shí)現(xiàn)python收發(fā)郵件功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01Selenium定時(shí)刷新網(wǎng)頁(yè)的實(shí)現(xiàn)代碼
這篇文章主要介紹了Selenium定時(shí)刷新網(wǎng)頁(yè)的實(shí)現(xiàn)代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10DataFrame 將某列數(shù)據(jù)轉(zhuǎn)為數(shù)組的方法
下面小編就為大家分享一篇DataFrame 將某列數(shù)據(jù)轉(zhuǎn)為數(shù)組的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-04-04python 使用tkinter與messagebox寫界面和彈窗
這篇文章主要介紹了python 使用tkinter與messagebox寫界面和彈窗,文章內(nèi)容詳細(xì),具有一的的參考價(jià)值,需要的小伙伴可以參考一下2022-03-03Python實(shí)現(xiàn)8個(gè)概率分布公式的方法詳解
在本文中,我們將介紹一些常見(jiàn)的分布(均勻分布、高斯分布、對(duì)數(shù)正態(tài)分布等)并通過(guò)Python代碼進(jìn)行可視化以直觀地顯示它們,感興趣的可以學(xué)習(xí)一下2022-05-05