资讯专栏INFORMATION COLUMN

生成器进化到协程 Part 2

fuyi501 / 2729人阅读

摘要:一个典型的上下文管理器类如下处理异常正如方法名明确告诉我们的,方法负责进入上下的准备工作,如果有需要可以返回一个值,这个值将会被赋值给中的。总结都是关于上下文管理器的内容,与协程关系不大。

Part 1 传送门

David Beazley 的博客

PPT 下载地址

在 Part 1 我们已经介绍了生成器的定义和生成器的操作,现在让我们开始使用生成器。Part 2 主要描述了如何使用 yieldcontextmanager 创建一个上下文管理器,并解释了原理。


上下文管理器

理解上下文可以联想我们做阅读理解时要解读文章某处的意思需要阅读该处前后段落,正是前后文提供了理解的“背景”。而程序的运行的上下文也可以理解为程序在运行时的某些变量,正是这些变量构成了运行环境,让程序可以完成操作。

Python 中的上下文管理器提供这样一种功能,为你的程序运行时提供一个特定“空间”,当进入这个空间时 Python 上下文管理器 为你做一些准备工作。这个“空间”中一般含有特殊的变量,你在这个“空间”中进行一些操作,然后离开。在你离开时 Python 上下文管理器又会帮你做一些收尾工作,保证不会污染运行环境。

下面是一些常见的代码模式

# 读取文件
f = open()
# do something
f.close()


# 使用锁
lock.acquire()
# do somethin
lock.release()


# 进行数据库操作
db.start_transaction()
# do something
db.commit()


# 对某段代码进行计时
start = time.time()
# do something
end = time.time()

这些代码进行的都是“先做这个(准备工作,比如获取一个数据库连接),然后做这个(比如写入数据),最后整理工作环境(如提交改动,关闭链接,释放资源等)。

如果使用 with 可以这样写:

witn open(filename) as f:
    # do something
    pass
    
with lock():
    # do something
    pass

with 语句实际上使用了实现了 __enter____exit__ 方法的上下文管理器类。一个典型的上下文管理器类如下:

clss ContextManager:
    def __enter__(self):
        return value
    def __exit__(self, exc_type, val, tb):
        if exec_type is None:
            return
        else:
            # 处理异常
            return True if handled else False

正如方法名明确告诉我们的,__enter__ 方法负责进入上下的准备工作,如果有需要可以返回一个值,这个值将会被赋值给 with ContextManager() as ret_value 中的 ret_value__exit__ 则负责收尾工作,这包括了异常处理。

对于这样一段代码

with ContextManager() as var:
    # do something

相当于

ctxmanager = ContextManager()
var = ctxmanager.__enter__()
# do somethin
ctxmanager.__exit__()

一个可用的例子:

import tempfile
import shutil

class TmpDir:
    def __enter__(self):
        self.dirname = tempfile.mkdtemp()
        return self.dirname
    
    def __exit__(self, exc, val, tb):
    shutil.rmtree(self.dirname)

这个上下文管理提供临时文件的功能,在 with 语句结束后会自动删除临时文件夹。

with TempDir() as dirname:
    # 使用临时文件夹进行一些操作
    pass

关于上面两个特殊方法的文档可以在 Python 文档的 Context Manager Types 找到。另外关于 with 关键字的详细说明参考 PEP 343,不过这篇 PEP 不是很好读,Good Luck :simple_smile:!

使用 yield 和 contextmanager

能看到这里的都应该对上下文管理器有所了解,准备好把 yield 加入我们的上下文管理器代码中。

先看一个例子

import tempfile, shutil
from contextlib import contextmanager

@contextmanager
def tempdir():
    outdir = tempfile.mkdtemp()
    try:
        yield outdir
    finally:
        shutil.rmtree(outdir)

与使用上下文管理器类的实现方式不同,这里我们没有显式实现 __enter____exit__,而是通过 contextmanager 装饰器和 yield 实现,你可以试试这两种方式是等价的。

要理解上面的代码,可以把 yield 想象为一把剪刀,把这个函数一分为二,上部分相当于 __enter__,下部分相当于 __exit__我这样说大家应该明白了吧。

import tempfile, shutil
from contextlib import contextmanager

@contextmanager
def tempdir():
    outdir = tempfile.mkdtemp() #
    try:                        # __enter__
        yield outdir            #
--cut---╳-----------------------------------
    finally:                    #
        shutil.rmtree(outdir)   # __exit__

实现“剪刀”功能关键在于 contextmanager 。对于上面的代码,我们来一步一步地结构它:

contextmanager 装饰器

contextmanager 其实使用了一个上下文管理器类,这个类在在初始化时需要提供一个生成器。

class GeneratorCM:
    def __init__(self, gen):
        self.gen = gen
    
    def __enter__(self):
       ...
        
    def __exit__(self, exc, val, tb):
        ...

contextmanager 的实现如下

def contextmanager(func):
    def run(*args, **kwargs):
        return GeneratorCM(func(*args, **kwargs))
    return run

由于 contextmanger 所装饰的函数里有 yield 所以我们在调用 func(*args, **kwargs) 时返回的是一个生成器。要使这个生成器前进,我们需要调用 next 函数

让生成器前进
def __enter__(self):
    return next(self.gen)

GeneratorCM__ente__ 方法会让生成器前进到 yield 语句处,并返回产出值。

收尾
def __exit__(self, exc, val, tb):
    try:
        if exc is None:
            next(self.gen)
        else:
            self.gen.throw(exc, val, tb)
        raise RuntimeError("Generator didn"t stop")
    except StopIteration:
        return True
    except:
        if sys.exc_info()[1] is not val: raise

__exit__ 函数的逻辑比较复杂,如果没有传入异常,首先它会尝试对生成器调用 next,正常情况下这会抛出 StopIteration ,这个异常会被不做并返回 True ,告诉解释器正常退出;如果传入异常,会使用 throwyield 处抛出这个异常;如果有其他未捕捉的错误,就重新抛出该错误。

实际的代码实现会更加复杂,还有一些异常情况没有处理

没有相关值的异常

在 with 语句块中抛出的 StopIteration

在上下文管理器中抛出的异常

如果你对怎么实现感兴趣,你可以阅读代码或者再一次阅读 PEP 343。

总结

Part 2 都是关于上下文管理器的内容,与协程关系不大。但通过这部分我们可以看到 yield 完全不同的用法,也熟悉了控制流 (control-flow) ,这与 Part 3 的异步处理流程有很大关系。让我们 Part 3 再见。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/41947.html

相关文章

  • 成器进化协程 Part 1

    摘要:生成器用于定义生成器函数只要存在该函数必定是一个生成器调用该函数返回一个生成器让一个生成器前进使用使一个生成器前进到下一个语句处,并将产出值作为其返回值。 前言 这篇文章大部分来自 David Beazley 在 PyCon 2014 的 PPT 《Generators: The Final Frontier》。这个PPT很长而且非常烧脑,建议在阅读前应了解 Python 的生成器与携...

    lemon 评论0 收藏0
  • PHP回顾之协程

    摘要:本文先回顾生成器,然后过渡到协程编程。其作用主要体现在三个方面数据生成生产者,通过返回数据数据消费消费者,消费传来的数据实现协程。解决回调地狱的方式主要有两种和协程。重点应当关注控制权转让的时机,以及协程的运作方式。 转载请注明文章出处: https://tlanyan.me/php-review... PHP回顾系列目录 PHP基础 web请求 cookie web响应 sess...

    Java3y 评论0 收藏0
  • tornado 源码之 coroutine 分析

    摘要:源码之分析的协程原理分析版本为支持异步,实现了一个协程库。提供了回调函数注册当异步事件完成后,调用注册的回调中间结果保存结束结果返回等功能注册回调函数,当被解决时,改回调函数被调用。相当于唤醒已经处于状态的父协程,通过回调函数,再执行。 tornado 源码之 coroutine 分析 tornado 的协程原理分析 版本:4.3.0 为支持异步,tornado 实现了一个协程库。 ...

    NicolasHe 评论0 收藏0
  • PyTips 0x13 - Python 线程与协程2

    摘要:项目地址我之前翻译了协程原理这篇文章之后尝试用了模式下的协程进行异步开发,确实感受到协程所带来的好处至少是语法上的。 项目地址:https://git.io/pytips 我之前翻译了Python 3.5 协程原理这篇文章之后尝试用了 Tornado + Motor 模式下的协程进行异步开发,确实感受到协程所带来的好处(至少是语法上的:D)。至于协程的 async/await 语法是如...

    史占广 评论0 收藏0
  • 图文 视频双管齐下,带你全面彻底理解Retrofit源码,Android开发五年

    摘要:协程的判断条件下面我们来着重看下的源码,因为从这里开始就涉及到协程的判断。第二点是关键点,用来判断该方法的调用是否使用到了协程。原理我们先来看下使用协程是怎么写的这是一个标准的协程写法,然后我们再套用上面的条件,发现完全匹配不到。 第一眼看,跟我之前印象中的有点区别(也不知道是什么版本),return的时候居然...

    不知名网友 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<