[Python学习日记-42] Python 中的生成器
简介
表达式生成器
函数生成器
用生成器实现并发编程
简介
Python 中的生成器(Generator)是一种特殊的迭代器,它又被成为惰性运算,它可以在迭代过程中动态生成值,而不需要事先把所有的值存储在内存中。生成器可以通过两种方式来定义:使用生成器表达式或函数生成器。下面我们一起来看看生成器到底是怎么回事吧。
表达式生成器
在前面学习了列表生成式,通过列表生成式,我们可以直接创建一个列表。但是会受到内存限制,所以列表容量肯定是有限的。如果创建一个包含100万个元素的列表,不仅会占用很大的存储空间,而且如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。例如要循环100万次,如果按 Python 的语法,代码如下
注意:Python3 对 range() 进行了优化,使用了生成器,所以我们要切换到 Python2 当中来查看
# Python2 的环境下
for i in range(1000000):
print(i)
# 会看到包含0-999999的列表的出现
print(range(1000000))
代码输出如下:
上面的代码会先生成100万个值的列表。假如现在循环到第50次时就不想继续就退出了。但是90多万的列表元素就白为你提前生成了。代码如下
# Python2 的环境下
for i in range(1000000):
if i == 50:
break
print(i)
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?像上面代码中的循环,每次循环只是 +1 而已。我们完全可以写一个算法,让他执行一次就自动 +1,这样就不必创建完整的列表了,从而节省大量的空间。在 Python 中,这种一边循环一边计算后面元素的机制,称为生成器(generator)。
要创建一个生成器,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [] 改成 (),就创建了一个生成器了,即生成器表达式,代码如下
# 列表生成式
l = [x * x for x in range(10)]
print(l)
# 生成器
l = (x * x for x in range(10))
print(l)
代码输出如下:
上面的代码当中 (x * x for x in range(10)) 生成的就是一个生成器了。可问题来了,我们可以直接打印出列表当中的每一个元素,但是我们怎么打印出生成器的每一个元素呢?
如果要一个一个打印出来,可以通过 next() 函数获得生成器的下一个返回值,代码如下
g = (x * x for x in range(10))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
代码输出如下:
前面我们讲过,生成器保存的是算法,每次调用 next(g) 就计算出 g 的下一个元素的值,直到计算到最后一个元素,当没有更多的元素时,则会抛出 StopIteration 的错误。当然,像上面代码那样不断地使用 next(g) 来获取下一个值实在是不太正常,正确的方法是使用 for 循环来获取,这是因为生成器也是一个可迭代(遍历)对象,代码如下
g = (x * x for x in range(10))
for i in g:
print(i)
代码输出如下:
函数生成器
生成器非常强大,但是如果推算的算法比较复杂,用生成器表达式生成的生成器配合 for 循环无法实现想要效果的时候,还可以用函数来实现生成器。例如著名的斐波拉契数列(Fibonacci),即除第一个和第二个数外,任意一个数都可由前两个数相加得到,如下
1,1,2,3,5,8,13,21,34,...
下面先试用普通代码来实现100以内的斐波拉契数列,代码如下
a,b = 0,1
n = 0 # 斐波那契数
while n < 100:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
print(n)
代码输出如下:
下面我们把上面的代码改写成函数看看应该如何实现呢?代码如下
def fib(max):
a,b = 0,1
n = 0 # 斐波那契数
while n < max:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
print(n)
fib(100)
代码输出如下:
从输出可以看出,fib() 函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似生成器。也就是说,上面的 fib() 函数和生成器仅一步之遥。要把 fib() 函数变成生成器,只需要把 print(n) 改为 yield n 就可以了,代码如下
def fib(max):
a,b = 0,1
n = 0 # 斐波那契数
while n < max:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
yield n # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒
f = fib(100) # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
print(next(f))
print(next(f))
print(f.__next__()) # next(f) 和 f.__next__() 是一样的
print(f.__next__())
代码输出如下:
这就是定义生成器的另一种方法,函数生成器。如果一个函数定义中包含 yield 关键字,那么这个函数就不再是一个普通函数,而是一个函数生成器。
这里,最难理解的就是生成器和函数的执行流程不一样。函数是顺序执行的,遇到 return 语句或者最后一行函数语句就返回。而变成生成器的函数后,在每次调用 next() 的时候函数才执行,遇到 yield 语句就暂停并返回数据到函数外,再次被 next() 调用时从上次返回的 yield 语句处继续执行。
我们尝试一下在总多 next() 和 __next__() 中插入一些别的事情,看看会不会有影响,代码如下
def fib(max):
a,b = 0,1
n = 0 # 斐波那契数
while n < max:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
yield n # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒
f = fib(100) # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
print(next(f))
print(next(f))
print(f.__next__()) # next(f) 和 f.__next__() 是一样的
print("干点别的事情")
print(f.__next__())
代码输出如下:
在上面输出中,我们在循环过程中不断调用 yield,函数就会不断的中断(暂停),即使中间有其他代码插进来运行了。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。同样的,把函数改成生成器后,我们基本上从来不会用 next() 来获取下一个返回值,而是直接
使用 for 循环来迭代,代码如下
def fib(max):
a,b = 0,1
n = 0 # 斐波那契数
while n < max:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
yield n # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒
f = fib(100) # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
for i in f:
print(i)
代码输出如下:
用生成器实现并发编程
虽然我们还没讲过并发编程,但我们肯定听过 CPU 多少核或者多少线程之类的,CPU 的多核就是为了可以实现并行运算的,就是让你同时边听歌、边聊 QQ、边刷知乎。而单核的 CPU 同一时间只能干一个事,所以你用单核电脑同时做好几件事的话,就会变的很慢,因为cpu要在不同程序任务间来回切换。
而通过函数生成器(yield),我们可以实现单核下并发做多件事的效果,即单线程多并发。在此之前我们要先说一个知识点,上面说了 yield 可以返回运算后的结果后暂停函数,那我们能不能把数据传进去然后暂停函数呢?这当然是可以的,我们只需要把 yield n 后面的 n 去掉,然后把 yield 赋值给一个变量就好了,并且传入的方法从 next()/__next__() 变为 send(),代码如下
def g_test():
while True:
n = yield # 收到的值给n
print("receive from outside:",n)
g = g_test()
g.__next__() # 调用生成器,同时会发送None到yield
for i in range(10):
# can't send non-None value to a just-started generator
# 生成器一开始需要发送一个None的值来激活yield,send()不能发送空值
g.send(i) # 调用生成器,同时发送i
代码输出如下:
上面的代码有几点需要注意的,yield 的输入要求一定要先运行 __next__() 或者 next(),然后再使用 send() 发送数据到函数,否则会抛出 TypeError 的错误,如下图所示
这是因为在没有进行第一次 next()/__next__() 之前函数并没有运行,就是说这个时候函数并没有进行初始化,而当运行了第一次 next()/__next__() 之后函数则会在 yield 处暂停,这个时候运行 send() 就会畅通无阻了。
说完这个知识点我们回到如何通过函数生成器(yield)实现单线程多并发的,我们以一个包子店的例子为例,我们的需求有以下几点:
- 包子店会接待多个消费者
- 包子店的师傅会在消费者吃包子的时候会生产一批包子
- 师傅做包子的时候消费者也在吃包子
代码如下
# 吃包子的消费者 c1,c2,c3
def consumer(name):
print("消费者%s准备吃包子啦。。。"%name)
while True:
baozi = yield # 接受外面的包子
print("消费者%s收到包子:%s"%(name,baozi))
c1 = consumer("C1")
c1.__next__()
c2 = consumer("C2")
c2.__next__()
c3 = consumer("C3")
c3.__next__()
for i in range(10):
print("--------生产了第%s批包子--------"%i)
c1.send(i)
c2.send(i)
c3.send(i)
代码输出如下:
从输出来看包子店的师傅在一边制作包子,一边分发给每一位顾客,并不是像普通的函数那样,先做好所有包子,然后再分发给顾客。函数生成器(yield)是如何实现单线程多并发的呢?其实它并不是同时在工作的,在 CPU 看来都是顺序作业,只不过速度非常快,从人的感觉来看就像是在并行工作一样。
他们的工作顺序是这样的:消费者 C1 到店——消费者 C2 到店——消费者 C3 到店——生产第1批包子——消费者 C1 收到包子:1——消费者 C2 收到包子:1——消费者 C3 收到包子:1——生产第2批包子——消费者 C1 收到包子:2——消费者 C2 收到包子:2——消费者 C3 收到包子:2——生产第3批包子...
如此类推,这就是通过函数生成器(yield)实现单线程多并发了