Python协程及其应用

看代码片段:

def mm(n):
   result = []
   for e in range(n):
      result.append(e)
   return result

if __name__=='__main__':
   for p in mm(100):
      print(p)

这个例子用来输出0~99共100个整数。代码看起来没有任何问题,但是如果把常数100改为1亿,则运行时要过很久才会打印出数据,更重要的是,内存中要准备好1亿个单位空间用来存放这1亿个数。解决问题的办法是使用yield语句:

def mm(n):
    for e in range(n):
        yield e

if __name__=='__main__':
   for p in mm(100000000):
      print(p)

运行这段代码,程序会很快打印数据,更重要的是,内存耗费也几乎没有增加。

类似于 return语句,yield <表达式>语句计算并且返回表达式的结果。Python把含有yield语句的函数称为生成器(Generator)。调用普通函数时会导致Python跳转到相应的函数体继续运行,而调用生成器函数则会得到一个生成器对象,而不是运行相应的函数体。比如上述代码中调用mm (100000000)并不会导致mm()函数体的执行,而是生成了一个能产生1亿个数的生成器对象[1]

当把一个生成器对象置于for循环中,就像上述代码中主程序那样,则每一次循环都会导致该生成器输出一个整数,并且该整数被赋值给循环变量。代码

for p in mm(10):
    print(p)

等价于:

genr = mm(10)
while True:
    try:
        p = next(genr)
    except StopIteration:
        break
    print(p)

也就是说,生成器genr在for循环中的使用,本质上是把genr作为参数调用内置函数next()。对于mm (10)所产生的生成器对象来说,next()最多只能调用10次。当第11次调用next()函数时就会产生StopIteration错误。Python用try ... except语句捕捉这个错误,一旦捕到,就执行break语句跳出循环。

是next(genr)语句而不是mm(10)才导致Python调用mm()的函数体,此时,运行堆栈(参见https://fanglin.blog.csdn.net/article/details/119202980)中会被压入一条运行记录rec,就像正常函数调用一样。当Python在mm()的函数体中执行到yield e语句时,Python会暂停在这一句之后,然后把运行记录rec保存在生成器的一个私有成员变量中,同时保存的还有代码的当前位置(即yield语句之后)。接着,Python弹出rec,把e作为返回值返回,就像return e所做的那样。于是调用方p=next(genr)就得到了这个返回值并赋值给变量p

当再次执行next(genr)时,Python首先在运行堆栈中恢复rec,然后程序从上次暂停的地方(即yield e之后)继续运行。当再次遇到yield语句时重复上述过程,从而保证每生成一个整数,就输出一个,而不是等全部10个整数都生成完毕再一次性返回。

2. 双向的生成器

生成器所产生的数据可以从生成方向使用方传递,而使用方也可以向生成方传递数据。方法是这样的:生成器执行形如abc = yield 123的语句,一方面输出123,另一方面接收使用方发出的数据,并且赋值给变量abc。而使用方应该执行xyz=genr.send(789),一方面接收数据123,另一方面发送数据789。请看下面代码:

双向发送数据示例

def my_genr(n):
    for i in range(n):
        a = yield i
        print('生成器收到:', a)

genr = my_genr(4)
x = 100
while True:
    b = genr.send(x)  # 第一次运行到这里就会报错
    x += 1
    print('使用方收到:', b)
    if b == 3:
        break

我们期望上述代码中的生成器能收到100~103四个整数,而使用方收到0~3四个整数,但实际运行时,第一次执行到genr.send(x)时就报错:can't send non-None value to a just-started generator。即不能发送一个非None的值给一个刚刚启动的生成器。原因在于genr.send()与next(genr)一样,都会导致生成器genr的函数体被执行,即进行一次函数调用。当Python在生成器函数体中遇到第一个yield语句时,会把它理解为返回语句return,不同点在于Python还把位于运行堆栈栈顶的运行记录和当前代码位置(即yield语句之后)保存在生成器中。所以形如abc=yield 123的语句等价于:

<保存栈顶运行记录>
保存下面return之后的语句位置
return 123
abc = <使用方发来的数据>

也就是说,在对变量abc赋值之前,函数已经退出了。这就是为什么上述genr.send()语句会报错的原因:此时发送的数据yield语句是接收不到的。纠正这个错误的方法有两种:令第一个.send()语句发送None,或者在.send()之前调用next(),即接收数据但不发送数据:

双向发送数据的正确做法

def my_genr(n):
    yield None  # 抵消第一个.send()或者next()调用
    for i in range(n):
        a = yield i
        print('生成器收到:', a)

genr = my_genr(4)
x = 100
next(genr)  # 保证生成器中的yield语句能接收到下面发送的数据
while True:
    b = genr.send(x)  # 不会报错
    x += 1
    print('使用方收到:', b)
    if b == 3:
        break

运行结果:

使用方收到: 0
生成器收到: 101
使用方收到: 1
生成器收到: 102
使用方收到: 2
生成器收到: 103
使用方收到: 3

3. yield from语句

有时,一个函数本身没有yield语句,但是它调用的一个子函数却含有yield语句,此时,从逻辑上前,当前函数也是会调用到yield语句的,但是它却不被认为是一个生成器,因为它的函数体代码中没有出现yield关键字。

这是因为Python是一种解释运行的语言,子函数在被调用前是不会被编译,不会被检查的。所以只有当当前函数运行到调用子函数时,Python才会去检查子函数的代码,才会发现它是一个生成器。

为了避免这个问题,Python允许用户使用yield from语句调用子函数,从而使得Python能够发现当前函数是一个生成器。比如:

def mm1():
   print('in mm1()')
   yield from mm2()

def mm2():
   yield 123
   yield 456

for e in mm1():
   print(e)

程序输出:

in mm1()
123
456

[1] 如果用参数1亿再一次调用mm()函数就会产生一个新的生成器对象,该对象同样能产生1亿个整数,但是与前一个相比是两个不同的生成器对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方林博士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值