一.Python中生成器
例如在处理列表生成式的时候
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
1.1 生成器的创建和调用
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[ ]改成(),就创建了一个generator:
a = (i for i in range(10))
print(a)
输出结果:
<generator object <genexpr> at 0x000001C87EDD5938>
生成器可以通过next()方法来回去下一个返回值
a = (i for i in range(10))
print(a)
print(next(a))
print(next(a))
print(next(a))
输出结果:
<generator object <genexpr> at 0x0000025D64905938>
0
1
2
generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。
当然,上面这种不断调用next(g)实在是太麻烦了,正确的方法是使用for循环,因为generator也是可迭代对象
所以创建了一个generator后,基本上永远不会调用next(),而是通过for循环来迭代它,并且不需要关心StopIteration的错误。
generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。
1.2 波拉契数列的生成器实现
比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到,直接上代码:
def fib(m):
n, a, b = 0, 0, 1
while n < m:
yield b
a, b = b, a + b
n += 1
return "end"
value_t = fib(6)
for v in value_t:
print(v)
输出结果:
1
1
2
3
5
8
1.3 通过yield关键字定义生成器
定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator,直接上代码:
def func_gen():
print("start into")
t = yield 15
print(t, "first")
c = yield 20
print(c, "two")
m = yield 25
print(m, "three")
c = func_gen()
i = 0
for v_obj in range(4):
if i == 0:
i += 1
print(c.send(None))
else:
i += 1
print(c.send("bb"))
执行结果:
start into
15
bb first
20
bb two
25
bb three
Traceback (most recent call last):
File "D:/work/my_gevent.py", line 34, in <module>
print(c.send("bb"))
StopIteration
可以看到func_gen()是generator,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用就报错
同样的,把函数改成generator后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代,例如上面斐波那契数列
def fib(m):
n, a, b = 0, 0, 1
while n < m:
yield b
a, b = b, a + b
n += 1
return "end"
value_t = fib(6)
for v in value_t:
print(v)
输出结果:
1
1
2
3
5
8
但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中
自动手动捕获异常,代码如下:
def fib(m):
n, a, b = 0, 0, 1
while n < m:
yield b
a, b = b, a + b
n += 1
return "end"
gen_obj = fib(6)
while True:
try:
x = next(gen_obj)
print("value", x)
except StopIteration as e:
print('Generator return value:', e.value)
break
输出结果:
value 1
value 1
value 2
value 3
value 5
value 8
Generator return value: end
通过我们自己手动捕获异常可以获取到迭代器中最终返回的值
1.4 杨辉三角形使用生成器实现
杨辉三角定义如下:
1
/ \
1 1
/ \ / \
1 2 1
/ \ / \ / \
1 3 3 1
/ \ / \ / \ / \
1 4 6 4 1
/ \ / \ / \ / \ / \
1 5 10 10 5 1
把每一行看做一个list,试写一个generator,不断输出下一行的list
def triangles():
l = [1]
while True:
yield l[:]
l.append(0)
l = [l[i-1] + l[i] for i in range(len(l))]
n = 0
results = []
for t in triangles():
results.append(t)
n = n + 1
if n == 10:
break
for t in results:
print(t)
if results == [
[1],
[1, 1],
[1, 2, 1],
[1, 3, 3, 1],
[1, 4, 6, 4, 1],
[1, 5, 10, 10, 5, 1],
[1, 6, 15, 20, 15, 6, 1],
[1, 7, 21, 35, 35, 21, 7, 1],
[1, 8, 28, 56, 70, 56, 28, 8, 1],
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
]:
print('测试通过!')
else:
print('测试失败!')
二.Python中协程
协程,又称微线程,英文名称为Coroutine
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行
综上,协程执行过程中可以通过子程序逻辑控制程序的执行和暂停
2.1 协程和多线程的区别
协程最大的优势就是协程极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了
2.2 协程实现消费者-生产者模型
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高,因为不存在线程切换,所有子程序的切换都是在程序内部自实现的
def consumer():
message = ""
while True:
rt = yield message
print("[Consumer] consumer is consuming %s" % rt)
message = "200 ok"
def produce(obj):
obj.send(None)
n = 0
while n < 5:
n += 1
print("[Produce] produce is producing %s" % n)
res = obj.send(n)
print("[Produce] consumer return:%s" % res)
obj.close()
if __name__ == '__main__':
c = consumer()
produce(c)
输出结果:
[Produce] produce is producing 1
[Consumer] consumer is consuming 1
[Produce] consumer return:200 ok
[Produce] produce is producing 2
[Consumer] consumer is consuming 2
[Produce] consumer return:200 ok
[Produce] produce is producing 3
[Consumer] consumer is consuming 3
[Produce] consumer return:200 ok
[Produce] produce is producing 4
[Consumer] consumer is consuming 4
[Produce] consumer return:200 ok
[Produce] produce is producing 5
[Consumer] consumer is consuming 5
[Produce] consumer return:200 ok
注意到consumer函数是一个generator,把一个consumer传入produce后:
首先调用c.send(None)启动生成器;
然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
consumer通过yield拿到消息,处理,又通过yield把结果传回;
produce拿到consumer处理的结果,继续生产下一条消息;
produce决定不生产了,通过c.close()关闭consumer,整个过程结束。
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
最后套用Donald Knuth的一句话总结协程的特点:子程序就是协程的一种特例