python with提前退出遇到的坑与解决方案


Posted in Python onJanuary 05, 2018

问题的起源

早些时候使用with实现了一版全局进程锁,希望实现以下效果:

with CacheLock("test_lock", 10):
  #如果抢占到锁,那么就执行这段代码
  # 否则,让with提前退出

全局进程锁本身不用多说,大部分都依靠外部的缓存来实现的,redis上用的是setnx,有时候根据需要加上缓存击穿问题、随机延后以防止对缓存本身造成压力

当时同样写了单元测试来测试这段代码的有效性:

with CacheLock("test_lock", 10):
  value = cache.get("test_lock")
  self.assertEqual(value, 1)
  with CacheLock("test_lock", 10):
    # 不会进到这里
    self.assertFalse(True)
value = cache.get("test_lock")
self.assertEqual(value, None)

看起来非常完美地通过了。

这样的一个全局进程锁是通过 __enter__ 方法抛出异常, __exit__ 方法中捕获异常来实现的:

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):
    #没有抢到锁的时候,啥都不做?
    if self.success:
      await cache.delete(self.lock_key)
    if isinstance(exc_value, LockException):
      return True
    if exc_type:
      raise exc_value

看起来还不错,毕竟单元测试都过了。

但是,这样的实现是有问题的:

原因在于 __exit__ 的执行不是包在 __enter__ 之外的,因此 __enter__ 抛出的异常,不会被 __exit__ 捕获。

上面的单元测试恰好通过,是因为其中有两个with语句,外面的with 捕获的其实是里面的 __enter__ 抛出的异常

使用改进后的单元测试:

cache.set("test_lock",1)
with CacheLock("test_lock", 10):
  self.assertFalse(True)
value = cache.get("test_lock")
self.assertEqual(value, None)

就会发现单元测试过不去了。

这个问题是我试图使用with实现另一个逻辑:AB测试 时出现的,同样是 __enter__ 抛出异常, __exit__ 试图捕获:

import operator
class EarlyExit(Exception):
  pass
class ABContext(object):
  """AB测试上下文
  >>> with ABContext(newVersion, consts.ABEnum.layer2):
  >>>   # dosomething
  """
  def __init__(self, version, ab_layer, relationship="eq"):
    self.version = version
    self.ab_layer = ab_layer
    # 如果不存在这种操作符,那就提前报错
    self.relationship = getattr(operator, relationship)
  def __enter__(self):
    # 如果不满足条件,等于不执行上下文中的内容
    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

调试没有通过的单元测试的时候发现,抛出异常后根本没有执行到 __enter__

第一种解决方案

既然想明白了with的执行顺序,那么第一种解决方案就呼之欲出了:既然__exit__捕获的异常在__enter__执行完成之后,那么我们提供一个函数确认一下就可以了,把ABContext实现改成这样:

def ensure(self):
    if not self.relationship(self.version, self.ab_layer.value):
      raise EarlyExit("not match")
  def __enter__(self):
    # 如果不满足条件,等于不执行上下文中的内容
    return self

使用的时候:

with ABContext(newVersion, consts.ABEnum.layer2) as c:
  c.ensure()
  # 执行其他的想要执行的代码

但这样的解决方法并不优雅,万一使用这个ABContext的时候忘记用ensure方法了,那么就等于完全没用这个Context方法,太容易失误了,而且代码也失去了Pythonic的性质

第二种解决方法

翻了一下contextlib的标准库文档,发现有一个已经废弃的函数: contextlib.nested

from contextlib import nested
with nested(*managers):
  do_something()

可以执行多个上下文.

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()

这个废弃的特性在Python2.7之后,可以直接由with关键字执行,形如:

with context1,context2:
  #do something

这个特性还不错,根据 __enter__ 的执行顺序的话,那么我们可以实现一个由第一个 context的__exit__来捕获,第二个context的__enter__来抛出异常,

如同这样:

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

结合前面我们实现的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,单元测试就这样过了

能不能再给力点?

确实,在with里要写俩context有点蛋疼,并不是特别优雅,能不能还是回到最初的那种用法:我们只用写一条context,这一个context做到了两个context的事情?

要是nested那个函数还在就好了。。要的其实就是它的功能。

Python3.1之后contextlib提供了一个ExitStack的功能来提供一个模拟的功能,但试了一下发现,实际上只调用了__enter__方法,但没有做对应的异常捕获

第三种解决方案

哈哈哈哈把自己绕到圈子里去了,想了一下,同样是一个缩进的代码块,为什么不能用if来解决呢!不就是个:

def test_context_noteq(self):
    # 不等的时候,不会执行with里的内容
    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

总之学到了contextlib里的一些有用的函数和装饰器,也第一次发现with可以放个context

虽然放多个context的动态构造还有待研究,with 后面的代码块也不能填一个元组或者列表。。惆怅。。

Python 相关文章推荐
python2.7删除文件夹和删除文件代码实例
Dec 18 Python
利用matplotlib+numpy绘制多种绘图的方法实例
May 03 Python
python实现学生信息管理系统
Apr 05 Python
python中format()函数的简单使用教程
Mar 14 Python
让代码变得更易维护的7个Python库
Oct 09 Python
12个步骤教你理解Python装饰器
Jul 01 Python
使用django实现一个代码发布系统
Jul 18 Python
使用Django清空数据库并重新生成
Apr 03 Python
Jupyter Notebook远程登录及密码设置操作
Apr 10 Python
Python使用pyexecjs代码案例解析
Jul 13 Python
python音频处理的示例详解
Dec 23 Python
一文搞懂如何实现Go 超时控制
Mar 30 Python
微信跳一跳小游戏python脚本
Jan 05 #Python
Python通过OpenCV的findContours获取轮廓并切割实例
Jan 05 #Python
Python+selenium实现截图图片并保存截取的图片
Jan 05 #Python
微信跳一跳辅助python代码实现
Jan 05 #Python
使用python为mysql实现restful接口
Jan 05 #Python
微信跳一跳python代码实现
Jan 05 #Python
python+opencv轮廓检测代码解析
Jan 05 #Python
You might like
mysql4.1以上版本连接时出现Client does not support authentication protocol问题解决办法
2007/03/15 PHP
PHP中几种常见的超时处理全面总结
2012/09/11 PHP
php发送get、post请求的6种方法简明总结
2014/07/08 PHP
PHP统计数值数组中出现频率最多的10个数字的方法
2015/04/20 PHP
学习php设计模式 php实现访问者模式(Visitor)
2015/12/07 PHP
PHP中16个高危函数整理
2019/09/19 PHP
不错的新闻标题颜色效果
2006/12/10 Javascript
JavaScript的ExtJS框架中表格的编写教程
2016/05/21 Javascript
用js控件div的滚动条,让它在内容更新时自动滚到底部的实现方法
2016/10/27 Javascript
jquery validation验证表单插件
2017/01/07 Javascript
利用Javascript实现简单的转盘抽奖
2017/02/13 Javascript
jQuery插件zTree实现单独选中根节点中第一个节点示例
2017/03/08 Javascript
JS实现获取图片大小和预览的方法完整实例【兼容IE和其它浏览器】
2017/04/24 Javascript
vue props传值失败 输出undefined的解决方法
2018/09/11 Javascript
VUE2.0+ElementUI2.0表格el-table实现表头扩展el-tooltip
2018/11/30 Javascript
基于Express框架使用POST传递Form数据
2019/08/10 Javascript
layui 实现表单和文件上传一起传到后台的例子
2019/09/16 Javascript
javascript+Canvas实现画板功能
2020/06/23 Javascript
JQuery基于FormData异步提交数据文件
2020/09/01 jQuery
vue+elementUI实现简单日历功能
2020/09/24 Javascript
vue 中使用print.js导出pdf操作
2020/11/13 Javascript
vue 项目@change多个参数传值多个事件的操作
2021/01/29 Vue.js
使用python实现正则匹配检索远端FTP目录下的文件
2015/03/25 Python
python实现支持目录FTP上传下载文件的方法
2015/06/03 Python
Python编程使用NLTK进行自然语言处理详解
2017/11/16 Python
python实现批量修改图片格式和尺寸
2018/06/07 Python
Pandas 按索引合并数据集的方法
2018/11/15 Python
Tensorflow实现神经网络拟合线性回归
2019/07/19 Python
pyqt5 QScrollArea设置在自定义侧(任何位置)
2019/09/25 Python
python GUI库图形界面开发之PyQt5线程类QThread详细使用方法
2020/02/26 Python
了解一下python内建模块collections
2020/09/07 Python
加拿大领先的优质厨具产品在线购物网站:Golda’s Kitchen
2017/11/17 全球购物
高三高考决心书
2014/03/11 职场文书
中国式结婚:司仪主持词(范文)
2019/07/25 职场文书
一文搞懂python异常处理、模块与包
2021/06/26 Python
pandas中pd.groupby()的用法详解
2022/06/16 Python