深入理解generator

关于iterrator的讨论就像js中介那个的闭包已经被讨论烂了.但是, 本文视图带给你一点新鲜的感受, 当然, 本文的适用人群仍然是刚入门python的新手. 高手请绕道之.

假设

为了保证阅读体验,

  • 假设你现在对iterator还不是很理解, 或者认为你已经理解, 但是不小心看到这篇文章的python新手.
  • 假设你有十分钟到十五分钟的时间.
  • 假设你不因为觉得作者傻逼中途走开.

那么, 亲爱的! 开始阅读吧哈哈

开始

generator有两种生成方式. 一种是用generator expression

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7f152f8362d0>

但是要注意如果把()换成[]得到的就是列表

>>> l = [x for x in range(10)]
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

还有一种方法是: 带yield的函数.
当我们定义函数的时候, 通常情况下执行return语句之后, 函数就返回到调用者了, 但是当我们将函数的return换成yield, 然后再调用的时候, 得到的就是一个generator

先看第一个函数

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print b
        a, b = b, a + b
        n = n + 1
    return 'done'

这个函数的功能是打印著名的斐波那契数列, 当我们在解释器调用这个函数的时候, 输出是这样子滴:

>>> fib(5)
1
1
2
3
5
8
done

我们知道斐波那契数列的原理是根据前面两项的值求得当前的值, 这种思想其实就是generator的思想, 只是调用generator不会一下子得到所有的数, 而是调用generator的next()方法, 得到下一个数列中的值.
那么, 我们把print改成yield, 得到下面的代码

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

然后再解释器调用

>>> f = fib(5)
>>> f
<generator object fib at 0x7f5dda0a0230>
>>> f.next()
>>> 1
>>> f.next()
>>> 1
>>> f.next()
>>> 2
>>> f.next()
>>> 3
...

从上面可以看到, 第一次调用next()的时候, 函数执行到yield b这一行, 然后就暂停了, 等待下一次调用generator的next(), 第二次调用next()的时候, b还是1, 所以yield b就返回了1, 以此类推
我开始知道generator这种神奇的存在的时候, 我想难道会有人不断调用next()阿狸获取返回值吗, 这不是没事找罪受吗, 实际上, 我们确实不会用yield来得到函数一下个返回值.而是用循环获取, 我会在稍后提到这个话题.
一般我们理解到这里就可以了, 但是如果generator就这么点儿尿性, 人们早就把他打入冷宫了.
好的, 为了更加深入理解, 我们接下来先再看一个函数

def f():
  print("yield begin...")
  for i in range(3):
    print('before yield {i}'.format(i=i))
    yield i
    print('after yield {i}'.format(i=i))

大家可以先猜一下函数的执行结果, 就算你觉得这个函数简单的一笔, 也先在我说出来或者你自己动手实践之前先猜测一下结果, 这是加深记忆的好方法, 因为人对自己做错的东西总是映像更深刻.
好了, 假设你心里已经有了答案. 实际上结果是这样的:

>>> ff.next()
yield begin...
before yield 0
0
>>> ff.next()
after yield 0
before yield 1
1
>>> ff.next()
after yield 1
before yield 2
2
...

我之所以举这个例子是想让大家记住, 每次调用next(), 函数都是在yield语句返回, 而不是继续执行到下一个yield语句之前, 我知道这是很合乎逻辑的, 但是为了刚好理解下面的内容, 我还是不厌其烦的拿出来讲一下.

send

事实上generator除了next()函数之外还有send函数. 这是干什么用的呢? 先看一下这个例子:

def ff():
    while True:
        val = yield
        print val * 10

def gg(g):
    for i in range(3):
        g.send(i)
        yield

理解这段代码可能有点困难, 我们不妨先理解第第一个函数

>>> def f():
...     while True:
...         val = yield
...         yield val*10
...
...
>>> foo = f()
>>> foo.next()
>>> foo.send(1)
10

我们第一次调用next的时候函数执行到yield语句, 仅仅是yield语句, 没有执行赋值, 然后我们调用generator的send方法, 就相当于给yield这个动作给了一个返回值. 你可能不理解为什么还可以把yield赋给一个变量, send的作用就在这里, 事实上我们不用send的时候, yield也是可以赋值的, 只不过相当于用None赋值.

>>> val = None
>>> print val * 10
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'

而如果我们如果对上述函数这样调用

>>> foo = f()
>>> foo.next()
>>> foo.next()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 4, in f
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int

得到了同样的错误. 第一个next 执行到第一个yield只是单纯的yield, 没有返回任何值 然后没有并没有调用send, 所以yield这个动作的值是None, 所以第二个next执行到yield val*10的时候就出现了错误. 因为重载的*不支持None和Int数据类型相乘.
所以, 你要注意并不是yield这个词有一个值, 而是yield这个动作有一个值, 就相当于主机和客户端通信, 我(客户端)yield给你(主机)一个数据, 你也可以给我返回一个值.
好, 理解到这里, 我们可以猜一下刚刚两个函数的执行结果了(事实上是一个函数反复调用另外一个), 添加一段测试代码

def ff():
    while True:
        val = yield
        print val * 10

def gg(g):
    for i in range(3):
        g.send(i)
        yield

if __name__ == '__main__':
    fff = ff()
    fff.send(None)
    p = gg(fff)
    for i in range(3):
        next(p)

然后保存为一个文件, 用python命令执行

% python fib.py
0
10
20

等等, 为什么fff(generator)还没有调用next(), fff就已经开始send了呢. 这不合逻辑啊. 我们可以这样理解一下
understande-send
意思就是说, 第一个send(None)的意思就是让迭代器准备好, 准备好的意思默认执行了第一个yield, 可以开始调用send了, 在这之后, 调用next和next总是成对出现的.我们来验证一下我们的理解.

>>> foo = f()
>>> foo.send(None)
>>> foo.next()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 4, in f
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'

应用

那么, 这个generator到底有什么用处呢?
最常见的应用就是在for循环迭代, 可以迭代的元素除了集合数据类型比如list, tuple, dict, str等等. 还有一种就是generator了.事实上, 这两种对象都属于iterable的后代. 也就是说python的for循环作用的对象都是iteratorable的, iterable相当于具备可迭代功能的mixin. generator也混入了这种功能.

当然了, generator还有一种重要的用途—-编写携程!!啊错了..是协程!
来看这个例子

def consumer():
    r = ''
    while True:
        n = yield
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'

def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

c = consumer()
produce(c)

这怎么又把send()赋给另外一个r了, 按照之前逻辑不难想通, 这个send的值就是下一次yield回来的值.
结果是这样的:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

如果我们把第一个函数的yield r改成, yield.按照我们的理解, 第二个函数的r就应该是None.我们来看一下结果
consumer-producer
果然是这样的!

所以协程的工作流程是这样的.
coroutine

相比多线程而言, 协程就是再一个线程里面, 不用切换线程, 而且一个协程给另一个协程send之后(交代某种任务), 也不用等待另一个协程执行完, 可以自己干自己的, 然后当另一个返回结果(yield)之后, 再处理结果. 这样的机制简直是完美!
perfect

总结

我们通过几个例子由浅入深的介绍了generator的特性, 原理, 应用.总的来说就是

  • yield相当于普通函数的return 不同的是可以通过send函数给yield表达式赋值
  • generator实际上存储的是计算下一个值的逻辑. for循环通过调用next来迭代.
  • generator可以用来编写协程.

感谢大家能够看完. 谢谢!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值