yield的一些探索

Python 小记 2018-12-21 9644 字 1129 浏览 点赞

开头

这篇内容看起来混乱,但其实是在为一个知识点服务:如何实现协程?

在有了明确的目标之后,就可以发现“混乱”都环绕在一个点上:如何手动切换函数的同时,捎上数据。

因能力有限,错漏处望各友不吝赐教。

概念

yield关键字可以让一个函数秒变生成器:

def generation_alpha():
    yield "z"
    yield "t"
    yield "y"

if __name__ == "__main__":
    myAlphas = generation_alpha()
    print(dir(myAlphas))

输出结果很长,这里我就只列出关键部分:[..., '__iter__', ..., __next__', ...]

这种时候,我们可以对myAlpha进行以下操作:

for alpha in myAlphas:
    print(alpha)

或者:

alphaOne = next(myAlphas)
print(alphaOne)

alphaTwo = next(myAlphas)
print(alphaTwo)

alphaThr = next(myAlphas)
print(alphaThr)

二者可以输出同样的结果。

然而,利用next()方法会带来一些麻烦。复用上边的例子,如果next()个数多于yield个数,会带来StopIteration异常:

if __name__ == "__main__":
    myAlphas = generation_alpha()
    next(myAlphas)
    next(myAlphas)
    next(myAlphas)
    next(myAlphas)  # 第四次next()

# 输出:
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 40, in <module>
    next(myAlphas)
StopIteration

利用for..in...不会有此异常,是因为for一直在等待这个异常,它等到了,所以知道了:“嗯,遍历该停止。”

yield 与 return

yield与return的区别恐怕不必多讲,现在我想说说混合使用它们的情况。就像这样:

def generation_alpha():
    yield "z"
    yield "t"
    return "y"

if __name__ == "__main__":
    myAlphas = generation_alpha()
    
    for alpha in myAlphas:
        print(alpha)

# 输出:
z
t

看起来字母y被抛弃了。由于for...in...将异常自行处理掉,现下我们并不清楚程序里边究竟做了什么。换next()试试!

def generation_alpha():
    yield "z"
    yield "t"
    return "y"

if __name__ == "__main__":
    myAlphas = generation_alpha()
    next(myAlphas)
    next(myAlphas)
    next(myAlphas)

# 输出:
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 39, in <module>
    next(myAlphas)
StopIteration: y

较前一次StopIteration比较,它旁边多一个y。其实我们可以通过捕获这个异常来获取generation_alpha()函数return出来的值:

def generation_alpha():
    yield "z"
    yield "t"
    return "y"

if __name__ == "__main__":
    myAlphas = generation_alpha()
    alphaOne = next(myAlphas)
    alphaTwo = next(myAlphas)
    try:
        next(myAlphas)
    except StopIteration as e:
        alphaThr = e.value

    print(alphaOne, alphaTwo, alphaThr)

# 输出:
z t y

生成器对象

可能这里得说下生成器对象的三个方法:send、close、throw

send方法

想拿到生成器中的数据不一定要for遍历以及next(),事实上send()也是不错的选择,重点在于:send()方法可以向生成器发送消息。如果说next是你的追求者对你一次次道晚安你却爱理不理;那么send就是你同男友甜蜜互动的工具,但这个时候,你得主动些。

我们假设一个场景吧,假设男女生讨论午饭吃点什么的场景。作为单身汉的我试着模拟这段对话:

def boy():
    ans1 = yield "男: 午饭吃什么呢?"
    print(ans1)  # 女生的第一次回答

    ans2 = yield "男: 法式沙拉怎么样?"
    print(ans2)
    
    ans3 = yield "男: 五...五花肉?"
    print(ans3)

    ans4 = yield "男: 所以你想吃什么?"
    print(ans4)

if __name__ == "__main__":
    girl = boy()  # 每个女生都应该对她的男友宣誓主权

    que1 = girl.send(None)  # 女生打电话给男友, 这个时候女生不需要说什么, 男友会主动找话题
    print(que1)

    que2 = girl.send("女: 随便.")
    print(que2)

    que3 = girl.send("女: 所以你觉得我胖咯.")
    print(que3)

    que4  = girl.send("女: 油腻了呢!")
    print(que4)

    que5 = girl.send("女: 随便.")
    print(que5)

输出结果如下:

男: 午饭吃什么呢?
女: 随便.
男: 法式沙拉怎么样?
女: 所以你觉得我胖咯.
男: 五...五花肉?
女: 油腻了呢!
男: 所以你想吃什么?
女: 随便.
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 58, in <module>
    que5 = girl.send("女: 随便.")
StopIteration

怎么程序抛异常了呢?我猜大概率上是男生崩溃了吧……等一下,我想正经一点!


事实上send()与next()起着相似作用,只是send()多了一个发出消息的功能。这个消息被赋值给yield左边的变量。yield起到的作用与赋值操作并不连贯。每一个send会让生成器停留在最近一个yield语句执行完的地方,直到下一个send将生成器激活往下走。

过程如图上所说的那样。那么能不能发送方发送消息,接收方选择不接受呢?我想一个男生在那儿自言自语并不违法,那么就自然是可以的:

def boy():
    # ans1 = yield "男: 午饭吃什么呢?"
    # print(ans1)  # 女生的第一次回答
    yield "男:午饭吃什么呢?"
    yield "男: 法式沙拉怎么样?"
    yield "男: 五...五花肉?"
    yield "男: 所以你想吃什么?"

if __name__ == "__main__":
    girl = boy()  # 每个女孩都应该对她的男友宣誓主权

    que1 = girl.send(None)  # 女生打电话给男友, 这个时候女生不需要说什么, 男友会主动找话题
    print(que1)

    que2 = girl.send("女: 随便.")
    print(que2)

    que3 = girl.send("女: 所以你觉得我胖咯.")
    print(que3)

    que4  = girl.send("女: 油腻了呢!")
    print(que4)

    que5 = girl.send("女: 随便.")
    print(que5)

# 输出:
男:午饭吃什么呢?
男: 法式沙拉怎么样?
男: 五...五花肉?
男: 所以你想吃什么?
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 53, in <module>
    que5 = girl.send("女: 随便.")
StopIteration

为男女平等起见,女孩也可以自言自语呢!

def boy():
    ans1 = yield
    print(ans1)

    ans2 = yield
    print(ans2)
    
    ans3 = yield
    print(ans3)

    ans4 = yield
    print(ans4)

if __name__ == "__main__":
    girl = boy()
    
    que1 = girl.send(None)
    que2 = girl.send("女: 你怎么不问我午饭吃什么?.")
    que3 = girl.send("女: 喂,说话呀!")
    que4  = girl.send("女: 死哪儿去了???")
    que5 = girl.send("女: 分手!!!")

# 输出:
女: 你怎么不问我午饭吃什么?.
女: 喂,说话呀!
女: 死哪儿去了???
女: 分手!!!
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 21, in <module>
    que5 = girl.send("女: 分手!!!")
StopIteration

差不多可以总结一下:

  • send是先将消息发送给上一个yield,然后接收下一个yield出来的数据。
  • send也可以不接受数据或认为接收的数据为None。
  • 第一个send发送的内容必须是None,因为它并不存在“上一个yield”
  • yield方可以选择不接受send发来的消息。
  • 如果yield个数与send个数保持一致,那么最后一个yield对左边变量的赋值操作将不会执行。

close

close方法可以关闭生成器的入口,这时候如果继续next,会引发StopIteration异常。比如:

def generation_alpha():
    yield "z"
    yield "t"
    yield "y"

if __name__ == "__main__":
    myAlphas = generation_alpha()

    print(next(myAlphas))
    myAlphas.close()  # 关闭生成器
    next(myAlphas)

# 输出:
z
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 13, in <module>
    next(myAlphas)
StopIteration

事实上,close方法是让生成器在挂起的地方引发GeneratorExit异常(这个异常继承自BaseException而不是Exception),如果在生成器中捕获这个异常同时想忽略它,从而继续执行后面的yield,解释器会说:“想得美!”

def generation_alpha():

    try:
        yield "z"
    except GeneratorExit:    
        pass
    
    yield "t"
    yield "y"

if __name__ == "__main__":
    myAlphas = generation_alpha()

    print(next(myAlphas))
    myAlphas.close()

# 输出:
z  # 注意,仍可以打印出字母z
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 17, in <module>
    myAlphas.close()
RuntimeError: generator ignored GeneratorExit  # 异常变成了RuntimeError

只是要求不可以有yield,你依然能做其他的逻辑处理。

def generation_alpha():

    try:
        yield "z"
    except GeneratorExit:    
        pass
    
    print("不好意思,我挂掉了!")

if __name__ == "__main__":
    myAlphas = generation_alpha()

    print(next(myAlphas))
    myAlphas.close()

# 输出:
z
不好意思,我挂掉了!

注:我尝试在后面的语句中添加return,然后去获取return出来的值,却始终拿不到。不知道各位有没有什么好法子。

throw

throw也是在生成器挂起的地方起作用,向生成器抛出一个异常,或许你可以这样用:

def generation_alpha():

    yield "z"
    yield 2
    yield "y"

if __name__ == "__main__":
    myAlphas = generation_alpha()

    for alpha in myAlphas:
        if not isinstance(alpha, str):
            myAlphas.throw(TypeError, "need str but int")
        print(alpha)
        
# 输出:
z
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 13, in <module>
    myAlphas.throw(TypeError, "need str but int")
  File "d:\MyCode\DEMO_TEST\name.py", line 5, in generation_alpha
    yield 2
TypeError: need str but int

yield from

yield from是python3.3添加的特性。

yield from [iterable]

# 等同于
for i in [iterable]:
    yield i

同时yield from默认处理掉StopIteration异常。

def generation_alpha():  # 子生成器
    yield "z"
    yield "t"
    yield "y"
    return "no more"

def generation():  # 委托生成器
    flag = yield from generation_alpha()
    print(flag)

if __name__ == "__main__":
    mygen = generation()
    
    for i in mygen:
        print(i)

# 输出:
z
t
y
no more

如开头所说的那样,for...in...会处理掉StopIteration,这种时候如果想拿到generation_alpha()中return出来的值,就不得不try一下(前边也讲述了这个方法)。然而,当你用上yield from之后,它负责帮你完成这个动作,区别在于:yield from并不负责帮你把StopIteration隐藏起来,而是转移了StopIteration的引发地(尽管这种说法似乎不够准确,但我实在想不到更好的阐述方式)。把for...in...换作next可见真知:

...
if __name__ == "__main__":
    mygen = generation()
    
    print(next(mygen))
    print(next(mygen))
    print(next(mygen))
    print(next(mygen))  # 注意第四个next

# 输出:
z
t
y
no more
Traceback (most recent call last):
  File "d:\MyCode\DEMO_TEST\name.py", line 19, in <module>
    print(next(mygen))
StopIteration

no more被打印出来,这说明flag确实拿到了子生成器return出来的值,但为什么依然抛异常了呢?

这是因为第三次使用next之后,生成器停在了yield "y"处,之后被第四个next激活,子生成器发现后面居然是return,那怎么行,我要抛异常了!却被委托生成器的yield from制止住。它们要私了,yield from提议说:“return出来的东西给我吧,以后谁找你要yield都让他来找我。”显然,上面的委托生成器并没有yield,所以异常屁颠屁颠地跑出来了。但要分清,这个StopIteration是从委托生成器跑出来的。

如果你对上述过程心存疑虑,不妨在之前的示例中的委托生成器里加上一个yield试试,就像下面这样:

...
def generation():
    flag = yield from generation_alpha()
    print(flag)
    
    yield "g"
...

因此在generation()(委托生成器)中使用yield from等效于以下方式:

...
def generation():
    alpha = generation_alpha()
    yield next(alpha)
    yield next(alpha)
    yield next(alpha)

    # 等同于: flag = yield from alpha
    try:
        yield next(alpha)
    except StopIteration as e:
        flag = e.value
    
    print(flag)
...

却不是完全相等。在我打断点调式的时候发现,通过yield from之后,主函数与子生成器直接交流,在到达return语句之前都没有委托生成器的事儿。

感谢



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论