Python中生成器以及协程实现

一.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的一句话总结协程的特点:子程序就是协程的一种特例

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值