同事在查看网络问题导致虚拟机状态一直pause时,在一段代码(见以下)处产生了疑惑。问我,我也是一头雾水。后同事找到参考文章(1),算是了解了个大概。而我对contextmanager的工作产生了兴趣,决定再稍稍弄清楚一点。

疑问代码:

with self.virtapi.wait_for_instance_event(
instance, events, deadline=timeout,
error_callback=self._neutron_failed_callback):
self.plug_vifs(instance, network_info)
self.firewall_driver.setup_basic_filtering(instance,
network_info)
self.firewall_driver.prepare_instance_filter(instance,
network_info)
domain = self._create_domain(
xml, instance=instance,
launch_flags=launch_flags,
power_on=power_on) self.firewall_driver.apply_instance_filter(instance,
network_info)
    @contextlib.contextmanager
def wait_for_instance_event(self, instance, event_names, deadline=300,
error_callback=None):
"""Plan to wait for some events, run some code, then wait. This context manager will first create plans to wait for the
provided event_names, yield, and then wait for all the scheduled
events to complete. Note that this uses an eventlet.timeout.Timeout to bound the
operation, so callers should be prepared to catch that
failure and handle that situation appropriately. If the event is not received by the specified timeout deadline,
eventlet.timeout.Timeout is raised. If the event is received but did not have a 'completed'
status, a NovaException is raised. If an error_callback is
provided, instead of raising an exception as detailed above
for the failure case, the callback will be called with the
event_name and instance, and can return True to continue
waiting for the rest of the events, False to stop processing,
or raise an exception which will bubble up to the waiter. :param:instance: The instance for which an event is expected
:param:event_names: A list of event names. Each element can be a
string event name or tuple of strings to
indicate (name, tag).
:param:deadline: Maximum number of seconds we should wait for all
of the specified events to arrive.
:param:error_callback: A function to be called if an event arrives
"""
if error_callback is None:
error_callback = self._default_error_callback
events = {}
for event_name in event_names:
if isinstance(event_name, tuple):
name, tag = event_name
event_name = external_event_obj.InstanceExternalEvent.make_key(
name, tag)
events[event_name] = (
self._compute.instance_events.prepare_for_instance_event(
instance, event_name))
yield
with eventlet.timeout.Timeout(deadline):
for event_name, event in events.items():
actual_event = event.wait()
if actual_event.status == 'completed':
continue
decision = error_callback(event_name, instance)
if decision is False:
break

yield在此有何用处? 为何需要yield呢?

第一个问题,可以从(1)中窥见一斑:

”任何在yield之前的内容都可以看做在代码块执行前的操作,而任何yield之后的操作都可以放在exit函数中。wait_for_instance_event ()就是

先准备计划等待一些event, 然后运行_create_domain_and_network()中提到的代码块,同时开始等待,等待超时后调用error_callback,

详细介绍在代码的注释中说的很清楚。“

至于为何需要yield呢?我们查看contextmanager的定义:

def contextmanager(func):
"""@contextmanager decorator. Typical usage: @contextmanager
def some_generator(<arguments>):
<setup>
try:
yield <value>
finally:
<cleanup> This makes this: with some_generator(<arguments>) as <variable>:
<body> equivalent to this: <setup>
try:
<variable> = <value>
<body>
finally:
<cleanup> """
@wraps(func)
def helper(*args, **kwds):
return GeneratorContextManager(func(*args, **kwds))
return helper

从用法上看,的确与上边引文描述的调用顺序一般。

略过@wraps装饰器(使用functools的partial实现对函数的装饰,以后会进一步学习)。继续深入:

class GeneratorContextManager(object):
"""Helper for @contextmanager decorator.""" def __init__(self, gen):
self.gen = gen def __enter__(self):
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("generator didn't yield") def __exit__(self, type, value, traceback):
if type is None:
try:
self.gen.next()
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
if value is None:
# Need to force instantiation so we can reliably
# tell if we get the same exception back
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration, exc:
# Suppress the exception *unless* it's the same exception that
# was passed to throw(). This prevents a StopIteration
# raised inside the "with" statement from being suppressed
return exc is not value
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But throw()
# has to raise the exception to signal propagation, so this
# fixes the impedance mismatch between the throw() protocol
# and the __exit__() protocol.
#
if sys.exc_info()[1] is not value:
raise

从代码上看,在调用wait_for_instance_event方法时,contextmanager装饰器会先调用该方法,将返回的结果作为参数,传递给GeneratorContextManager类进行初始化。GeneratorContextManager类定义了__enter__和__exit__方法,并在其中调用了self.gen.next()方法。这里,我又产生了疑问:1、wait_for_instance_event方法返回值是什么?它应该支持调用next()方法。2、__enter__是在什么时候调用的?

先来回答问题2,我引用了参考文章(2)的一段话:

基本语法和工作原理

with 语句的语法格式如下:

清单 1. with 语句的语法格式
    with context_expression [as target(s)]:
with-body

这里 context_expression 要返回一个上下文管理器对象,该对象并不赋值给 as 子句中的 target(s) ,如果指定了 as 子句的话,会将上下文管理器的 __enter__() 方法的返回值赋值给 target(s)。target(s) 可以是单个变量,或者由“()”括起来的元组(不能是仅仅由“,”分隔的变量列表,必须加“()”)。

Python 对一些内建对象进行改进,加入了对上下文管理器的支持,可以用于 with 语句中,比如可以自动关闭文件、线程锁的自动获取和释放等。假设要对一个文件进行操作,使用 with 语句可以有如下代码:

清单 2. 使用 with 语句操作文件对象
    with open(r'somefileName') as somefile:
for line in somefile:
print line
# ...more code

这里使用了 with 语句,不管在处理文件过程中是否发生异常,都能保证 with 语句执行完毕后已经关闭了打开的文件句柄。如果使用传统的 try/finally 范式,则要使用类似如下代码:

清单 3. try/finally 方式操作文件对象
    somefile = open(r'somefileName')
try:
for line in somefile:
print line
# ...more code
finally:
somefile.close()

比较起来,使用 with 语句可以减少编码量。已经加入对上下文管理协议支持的还有模块 threading、decimal 等。

PEP 0343 对 with 语句的实现进行了描述。with 语句的执行过程类似如下代码块:

清单 4. with 语句执行过程
    context_manager = context_expression
exit = type(context_manager).__exit__
value = type(context_manager).__enter__(context_manager)
exc = True # True 表示正常执行,即便有异常也忽略;False 表示重新抛出异常,需要对异常进行处理
try:
try:
target = value # 如果使用了 as 子句
with-body # 执行 with-body
except:
# 执行过程中有异常发生
exc = False
# 如果 __exit__ 返回 True,则异常被忽略;如果返回 False,则重新抛出异常
# 由外层代码对异常进行处理
if not exit(context_manager, *sys.exc_info()):
raise
finally:
# 正常退出,或者通过 statement-body 中的 break/continue/return 语句退出
# 或者忽略异常退出
if exc:
exit(context_manager, None, None, None)
# 缺省返回 None,None 在布尔上下文中看做是 False
  1. 执行 context_expression,生成上下文管理器 context_manager
  2. 调用上下文管理器的 __enter__() 方法;如果使用了 as 子句,则将 __enter__() 方法的返回值赋值给 as 子句中的 target(s)
  3. 执行语句体 with-body
  4. 不管是否执行过程中是否发生了异常,执行上下文管理器的 __exit__() 方法,__exit__() 方法负责执行“清理”工作,如释放资源等。如果执行过程中没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 __exit__(None, None, None) ;如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback)
  5. 出现异常时,如果 __exit__(type, value, traceback) 返回 False,则会重新抛出异常,让with 之外的语句逻辑来处理异常,这也是通用做法;如果返回 True,则忽略异常,不再对异常进行处理

结合引文中的清单1和清单4,可以看出,是先执行了self.virtapi.wait_for_instance_event(...)中的代码,并将返回值赋给context_manager,稍后保存了context_manager类的__exit__方法,然后执行context_manager类的__enter__方法,再执行with_body,最后根据情况执行__exit__。

这里的__enter__方法,实际上就是去调用wait_for_...方法的返回值的next方法。wait_for_...方法的返回值是什么?是yield时给函数的返回值吗?它为何竟能支持next方法呢?

在回答这个问题之前,我们先来一段试验代码:

>>> def gens(N):
... for i in range(N):
... yield i**2
...
>>> gens(3)
<generator object gens at 0x7fb3ac98ec30>
>>> s = gens(3)
>>> s
<generator object gens at 0x7fb3ac98ee60>
>>> s.next()
0
>>> s.next()
1
>>> s.next()
4
>>> s.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> dir(s)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']
>>> def gent(N):
... for i in range(N):
... i = i**2
...
>>> gent(3)
>>> print gent(3)
None
>>> dir(gent(3))
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

对比上述两个方法,可以看出,加入yield之后,函数的返回,实际上不是一个值,而是一个generator object(生成器对象)。这个对象,本身是支持next方法的。这就回答了上面问题1。

yield究竟是什么鬼?

以下内容来自Mark Lutz的《learning Python》:

Python的迭代协议:可迭代的对象定义了一个__next__方法,它要么返回迭代中的下一项,或者引发一个特殊的StopIteration异常来终止迭代。

要支持这一协议,函数包含一条yield语句,该语句特别编译为生成器。当调用时,它们返回一个迭代器对象,该对象支持用一个名为__next__的自动创建的方法来继续执行的接口。生成器函数也可能有一条return语句,总是在def语句块的末尾,直接终止值的生成。从技术上讲,可以在任何常规函数退出执行之后,引发一个StopIteration异常来实现。从调用者的角度来看,生成器的__next__方法继续函数并且运行到下一个yield结果返回或引发一个StopIteration异常。

直接效果就是生成器函数,编写为包含yield语句的def语句,自动支持迭代协议,并且由此可能用在任何迭代环境中以随着时间并根据需要产生结果。

截止目前,大致知道了本文开始两段代码的执行顺序。可惜的是,代码的内容依然未懂。下次再看。

如有错误之处,敬请指正。

参考文章:

(1)http://blog.csdn.net/epugv/article/details/44872583

(2)http://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/index.html

(3)https://docs.python.org/2/library/contextlib.html

理解contextmanager的更多相关文章

  1. Python3 与 C# 并发编程之~ 线程篇

      2.线程篇¶ 在线预览:https://github.lesschina.com/python/base/concurrency/3.并发编程-线程篇.html 示例代码:https://gith ...

  2. 点读系列《流畅的python》

    第1章 python数据模型 python的写法是由背后的魔法方法实现的,比如obj[key],解释器实际调用的是obj.__getitem__(key) 作者把魔法方法叫做双下方法,因为有两个下划线 ...

  3. Python中 __init__的通俗解释?附修饰器contextmanager的理解

    作者:匿名用户链接:https://www.zhihu.com/question/46973549/answer/103805810来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请 ...

  4. Python标准模块--ContextManager

    1 模块简介 在数年前,Python 2.5 加入了一个非常特殊的关键字,就是with.with语句允许开发者创建上下文管理器.什么是上下文管理器?上下文管理器就是允许你可以自动地开始和结束一些事情. ...

  5. 深入理解 Python 中的上下文管理器

    提示:前面的内容较为基础,重点知识在后半段. with 这个关键字,对于每一学习Python的人,都不会陌生. 操作文本对象的时候,几乎所有的人都会让我们要用 with open ,这就是一个上下文管 ...

  6. django 模板context的理解

    context作为view与template之间的桥梁,理解它的工作原理对于djagno的模板工作机制至关重要. class ContextDict(dict):#上下文词典,由词典可以通过conte ...

  7. 全面理解Python中的类型提示(Type Hints)

    众所周知,Python 是动态类型语言,运行时不需要指定变量类型.这一点是不会改变的,但是2015年9月创始人 Guido van Rossum 在 Python 3.5 引入了一个类型系统,允许开发 ...

  8. 理解CSS视觉格式化

    前面的话   CSS视觉格式化这个词可能比较陌生,但说起盒模型可能就恍然大悟了.实际上,盒模型只是CSS视觉格式化的一部分.视觉格式化分为块级和行内两种处理方式.理解视觉格式化,可以确定得到的效果是应 ...

  9. 彻底理解AC多模式匹配算法

    (本文尤其适合遍览网上的讲解而仍百思不得姐的同学) 一.原理 AC自动机首先将模式组记录为Trie字典树的形式,以节点表示不同状态,边上标以字母表中的字符,表示状态的转移.根节点状态记为0状态,表示起 ...

随机推荐

  1. v-model和sync修饰符

    场景: 在用vue开发的过程中我们经常会遇到父子组件共用同一变量的情况,那么在这种情况下,我们肯定会想直接 把变量传过来用,因为是双向绑定的所以子组件就会修改这个变量,这样在vue中时会报错的. 问题 ...

  2. viewport简介

    Viewport的用处:手机拥有了浏览器的初期,人们并没有专门为移动设备设计页面,造成的直接结果就是,访问的页面是直接将电脑页面进行缩放,操作起来有诸多不便,viewport就是用来解决这个问题的 1 ...

  3. 【spark】RDD创建

    首先我们要建立 sparkconf 配置文件,然后通过配置文件来建立sparkcontext. import org.apache.spark._ object MyRdd { def main(ar ...

  4. angular之 $watch 和$digest 和$apply和$observe的区别

    $watch 代表的就是对数据源的监听,当数据源发生变化,就会触发第二个参数的回调函数 $digest 代表触发一个数据源变化的事件 $apply 代表对于$digest的一个封装,他多了一个参数 . ...

  5. 将DLL注册到GAC

    使用方法如下: 方法1: (1).开始菜单->Microsoft Visual Studio 2010 ->Visual Studio Tools->Visual Studio Co ...

  6. docker 国内加速器配置

    配置镜像加速器 阿里云 登录到 阿里云获取到专属加速地址bqr1dr1n.mirror.aliyuncs.com 找到服务 deamon.js 所在目录C:\ProgramData\docker\co ...

  7. 条款28:避免返回handles指向对象的内部成分。

    首先看看下面这个例子: class Point{ public: point(int x, int y); ... void setX(int newVal); void setY(int newVa ...

  8. 卷积神经网络实战-----0001(移植卷积神经网络c++ to python or java)

    1. https://github.com/174high/simple_cnn  自己fork的 2. https://github.com/can1357/simple_cnn   最初始的 3. ...

  9. Nginx 日志分析及性能排查

    Nginx 日志分析及性能排查 2017-03-04 Linux爱好者 (点击上方公众号,可快速关注) 作者:-外星人- my.oschina.net/362228416/blog/844713 如有 ...

  10. [转载] ffmpeg Windows下采集摄像头一帧数据,并保存为bmp图片

    这里请注意,在编译ffmpeg时,不要使用--disable-devices选项. 使用 --enable-encoder=rawvideo --enable-decoder=rawvideo 启用r ...