定义
生成器其实也是一个函数:
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,如果用递归是可以直接取出里面的值的。故而这种不用保存现场的生成器方式要快的多。
这是生成器的第二种用法,用来解决递归问题。
高阶应用,协程
协程是生成器的高级用法。
线程是指进程内的一个执行单元,也是进程内的可调度实体。
线程与进程的区别:
- 地址空间:
- 进程内的一个执行单元
- 进程至少有一个线程
- 它们共享进程的地址空间,而进程有自己独立的地址空间
- 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源。
- 线程是处理器调度的基本单位,但进程不是
- 二者均可并发执行
协程也是类似这种东西,用来做调度的。
协程避免了无意义的调度,由此可以提高性能,但也因此程序员必须自己承担调度的责任。
同时,协程也失去了标准线程使用多CPU的能力。
进程和线程是内核态调度的,而协程是用户态调度的。
协程运行在一个线程之内,在用户态调度。(调度就是由调度器来决定哪段代码占用cpu时间)
用yield就可以实现调度器。yield有一个特点,会暂停,即让出cpu时间。
用next函数,执行到yield这里就暂停了,让出cpu,这个时候就可以由用户来决定干嘛。
只有执行到yield,才会让出,这种又称为非抢占式调度
小结
生成器感觉不太好掌握,都不是看个一两遍就能弄懂的事情啊。
async和await 这2个关键字底层都用了yield关键字,只是语义上的改变。