理解contextmanager

同事在查看网络问题导致虚拟机状态一直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
View Code

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
View Code

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

略过@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
View Code

从代码上看,在调用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']
View Code

 

>>> 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__']
View Code

对比上述两个方法,可以看出,加入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

posted on 2016-11-27 21:17  lisalala  阅读(1697)  评论(0编辑  收藏  举报

导航