Python学习笔记--生成器

定义

生成器其实也是一个函数:

def g():
    for x in range(10):
        yield x

>>> r = g()
>>> r
<generator object g at 0x0000027FC9865148>

这里定义了一个g函数,用到了yield没有return。可以看到返回了一个generator。有__it__方法是一个可迭代对象,有__next__方法,就是一个迭代器。

>>> next(r)
0
>>> next(r)
1
>>> for x in r:
...     print(x)
...
2
3
4
5
6
7
8
9

这里可以发现yield表示弹出一个值,而yield就是生成器。

原理

>>> def gen():
...     print('a')
...     yield 1
...     print('b')
...     yield 2
...     return 3
...
>>> g = gen()

#这里并没有返回值,且返回了一个generator说明函数并没有被执行。
>>> g
<generator object gen at 0x0000027FC9865948>

#这里发现执行到第一个yield,就停止执行了
>>> next(g)
a
1
#这里发现执行到第二个yield,就停止执行了,且并没有打印'a'
>>> next(g)
b
2
#再次执行之后就出现了报错。
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: 3

带yield语句得函数称之为生成器函数,生成器函数的返回值是生成器

特点:

  • 生成器函数执行的时候,不会执行函数体(注意,这里说的是生成器函数执行的时候,不是生成器执行的时候)
  • 当next生成器的时候,当前代码会执行到第一个yield,会弹出值,并且暂停函数
  • 当再次执行next生成器的时候,从上次暂停处开始往下执行
  • 当没有多余的yield的时候,会抛出StopIteration异常,如果函数有返回值,异常的value是函数的返回值

惰性求值

#写一个计数器
def counter():
    x = 0
    while True:
        x += 1
        yield x

def inc(c):
    return next(c)
  
>>> c = counter()
>>> inc(c)
1
>>> inc(c)
2

这样,每次就可以得到一个+1的值,还可以进行封装

def inc():
    c = counter()
    return lambda: next(c)
>>> incr = inc()
>>> incr()
1
>>> incr()
2

此外,我们还可以吧counter封装到inc里面

>>> def inc():
...     def counter():
...         x = 0
...         while True:
...             x += 1
...             yield x
...     c = counter()
...     return lambda: next(c)
...
>>> incr = inc()
>>> incr()
1
>>> incr()
2

此外不需要告诉别人inc是怎么实现的,只需要知道,每调用一次就会increase一次

难点

#注意这里没有加匿名函数
def make_inc():
    def counter():
        x = 0
        while True:
            x += 1
            yield x
    c = counter()
    return next(c)

#结果如下:
>>> make_inc()
1
>>> make_inc()
1

这里可以发现值并不会增加。那为什么会导致这种差别呢

>>> inc
<function inc at 0x0000027FC98D11F8>
>>> make_inc
<function make_inc at 0x0000027FC98D10D8>
>>> inc()
<function inc.<locals>.<lambda> at 0x0000027FC98D1318>
>>> make_inc()
1

这里每次调用的时候都对其进行了初始化,给他一个新的生成器,所以每次都是1.
而我们用lambda的话,他只会生成一次c,后面每次都是调用这个函数。

  • 用了一个无限大的列表来不断的计数下去
  • 并没有全局变量
  • 对于counter来说,就是一个闭包
  • c就是一个counter的实例
  • 在最后return的这个匿名函数里,每次执行都是引用这个生成器

应用

普通应用

对于斐波那契数列来说,使用递归实现是非常慢的,如下:

def fib(n):
    if n == 0:
        return 1
    if n == 1:
        return 1
    return fib(n-1) + fib(n-2)

换成生成器来写会快很多,如下:

>>> def fib():
...     a = 0
...     b = 1
...     while True:
...         a, b = b, a+b
...         yield a
...
>>> f = fib()
>>> f
<generator object fib at 0x000001F5C81E5148>
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3

这种用法就是一种递归循环,解决递归问题。

  • 没有递归深度限制
  • 递归的缺点都没有
  • 不需要保存现场
>>> ret = []
>>> for _ in range(1000):
...     ret.append(next(f))
...
> ret[4]
34

如上便是将现场保存到了ret,如果用递归是可以直接取出里面的值的。故而这种不用保存现场的生成器方式要快的多。
这是生成器的第二种用法,用来解决递归问题。

高阶应用,协程

协程是生成器的高级用法。
线程是指进程内的一个执行单元,也是进程内的可调度实体。
线程与进程的区别:

  • 地址空间:
    1. 进程内的一个执行单元
    2. 进程至少有一个线程
    3. 它们共享进程的地址空间,而进程有自己独立的地址空间
  • 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源。
  • 线程是处理器调度的基本单位,但进程不是
  • 二者均可并发执行

协程也是类似这种东西,用来做调度的。
协程避免了无意义的调度,由此可以提高性能,但也因此程序员必须自己承担调度的责任。
同时,协程也失去了标准线程使用多CPU的能力。
进程和线程是内核态调度的,而协程是用户态调度的。
协程运行在一个线程之内,在用户态调度。(调度就是由调度器来决定哪段代码占用cpu时间)
用yield就可以实现调度器。yield有一个特点,会暂停,即让出cpu时间。
用next函数,执行到yield这里就暂停了,让出cpu,这个时候就可以由用户来决定干嘛。
只有执行到yield,才会让出,这种又称为非抢占式调度

小结

生成器感觉不太好掌握,都不是看个一两遍就能弄懂的事情啊。
async和await 这2个关键字底层都用了yield关键字,只是语义上的改变。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值