同事在查看网络问题导致虚拟机状态一直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. php中POST与GET区别

    如果有人问你,GET和POST,有什么区别?你会如何回答? 我的经历 前几天有人问我这个问题.我说GET是用于获取数据的,POST,一般用于将数据发给服务器之用. 这个答案好像并不是他想要的.于是他继 ...

  2. python学习笔记(pict+requests+xml)

    博主尝试了下更换python版本 之前很多脚本改正运行错误后.还是不能正常运行 忙会了半天还是没有成功 只好还原版本 所以下面的代码还没实际运行成功.先记录下 #!/usr/bin/env pytho ...

  3. 负载均衡之DNS域名解析

    转载请说明出处:http://blog.csdn.net/cywosp/article/details/38017027 在上一篇文章(http://blog.csdn.net/cywosp/arti ...

  4. dotnet core项目升级到 .net core 2.0

    这几天无疑我们已经让.net core 2.0正式版发布的消息刷屏,这次发布整整提前了一个月的时间,关于具体的发布信息,可以去看善友大神的博客,.NET Core 2.0 正式发布信息汇总,废话不多说 ...

  5. MySQL乱码问题及字符集实战

    mysql> create database oldboy;Query OK, 1 row affected (0.01 sec) mysql> mysql> mysql> s ...

  6. Centos6.8 JDK配置

    记录一下在这个服务器配置的过程 ssh root@IP Password --------------------------------------------------------------- ...

  7. Python label for _ 用法

    Python label for _ 用法 Python label for _ 用法 >>> label_data = [iter([3,4]),iter([4,9]), iter ...

  8. VSCode打开文件总是会覆盖上次打开的标签

    在使用VSCode的时候,打开一个文件之后,如果没有修改的话,那么再打开下一个文件的时候,他总会替换上次打开的标签,那么怎么样才能每次都在新的标签打开文件呢? 实际上,这种情况的出现是因为我们点击文件 ...

  9. Supervisor进程监控

    安装 yum install -y python-setuptools easy_install supervisor echo_supervisord_conf > /etc/supervis ...

  10. 21天学通C++_Day1

    被阿里实习生的第一轮电话面试刷掉以后,幡然醒悟,发现以前学习的C++基础一点都不扎实.为了把基础打扎实,重新学习一遍:为了让自己不放弃,也顺便可以把当天学到的东西记录下来,开始了写博客. 学习书籍:& ...