【摘要】通过列表生成式,可以直接创建一个列表,但是受内存限制,列表容量都是有限的。而且,创建一个包含100万个元素的列表,不仅占用空间很大,如果仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白费了。python中,有一种一边循环一边计算的机制,称为生成器(generator)
1.生成器的创建
方法一:直接把一个列表生成式的[ ]改成( ),即可创建一个generator
# 生成器创建
nums_gen = (num for num in range(1, 10001) if num % 8 == 0)
print(nums_gen) # <generator object <genexpr> at 0x7f8f2cb92350>
print(type(nums_gen)) # <class 'generator'>
# 查看一个对象是否可以for循环?
from collections.abc import Iterable
print("生成器是否为可迭代对象?", isinstance(nums_gen, Iterable))
想要打印出generator中的每一个元素,可以通过generator的next( )方法(这个方法在python2中的调用语句是generator.next( )或者next(generator),但是在python3中只能用next(generator)调用)。当然也可以__next__()方法,这是面向对象中的魔术方法,等日后学习了面向对象再说。
下图是在python3中的调用:
下图是在python2中的调用:
generator保存的是算法,每次调用next( ),就可以计算出下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出Stoplteration的错误。当然,调用next( )方法取值实在太麻烦,正确打印的方法是使用for循环,因为generator也是可迭代对象:
方法二:定义一个函数,使用yield关键字让这个函数变成生成器
generator非常强大,如果推算的算法比较复杂,如果用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。比如斐波拉契数列,用列表生成式写不出来,但可以用函数把它打印出来。
下面这个函数是斐波那切数列的非递归实现,之前有同学面试被问到过,最好掌握一下!!!
fib( )函数是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。也就是说,上面的函数和generator仅一步之遥。要把fib()函数变成generator,只需要把print(b)改成yield(b) 或者 yield b。
看一下fib(10)的类型
如果函数中有yield关键字, 那么函数的返回值是生成器对象.
fib数列进阶,如何打印出指定大小的fib数列?eg:要求打印出小于100的数列。代码如下:
from itertools import takewhile
def fib():
a = b = 1
while True:
yield a
a,b = b,a+b
result = takewhile(lambda x:x<100,fib())
"""
takewhile(predicate, iterable) --> takewhile object
Return successive entries from an iterable as long as the
predicate evaluates to true for each entry.
"""
print(list(result))
强调takewhile这个小技巧的使用,传入一个predicate和一个iterable,从iterable中返回满足predicate的所有值。
2. yield关键字的工作流程
如果在调用函数的时候,发现这个函数中有yeild,那么此时,也就不是调用函数了,而是创建了一个生成器对象。
最难理解的是generator和函数的执行流程是不一样的。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next( )方法的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
就下边的代码一起走一遍流程就清楚了:
"""
理解yiled关键原理:
函数中包含yield关键字,返回的是生成器对象。
当第一次调用next(genObj)时,才开始执行函数内容;遇到yield关键字,执行停止。
再次调用next(genObj)时,从上次停止的代码位置继续开始执行,直到遇到yield,再次停止执行。
"""
def yield_example():
for count in range(100):
yield 'step'+str(count+1)
print('success')
result = yield_example()
print(next(result))
print(next(result))
第二个复杂例子:
def creat_num(all_num):
print('~~~~~~~~~~~1~~~~~~~~~~~~~~~~~')
a,b=0,1
current_num = 0
while current_num < all_num:
print('~~~~~~~~~2~~~~~~~~~~~~~~~~~')
yield a # 相当于暂停了程序
print('~~~~~~~~~~~~~~3~~~~~~~~~~~~')
a,b = b,b+a
current_num += 1
print('~~~~~~~~~~~4~~~~~~~~~~~~~~~~')
obj = creat_num(5)
print(obj)
ret = next(obj)
print(ret)
ret1 = next(obj)
print(ret1)
ret2 = next(obj)
print(ret2)
ret3 = next(obj)
print(ret3)
ret4 = next(obj)
print(ret4)
执行结果如下:
-
第一步:首先给create_num函数传入5,生成一个名为obj的生成器。函数从上至下顺序执行,print(‘~~1~~’)和print(‘~~2~~’),执行到yield语句,程序暂停,将变量a的值返回给obj,所以print(obj)的结果是0。
-
第二步:对obj这个生成器执行next()方法,将得到的值赋给ret。next()方法使得暂停的程序继续从yield语句后面开始执行,print(‘~~3~~’),
b的值赋给a,b+a的值赋给b,current_num 加1。由于此时依旧满足current_num (1)< all_num(5),所以继续执行while循环里的语句。print(‘~~2~~’),执行到yield语句暂停程序,将a的值返回赋给ret。打印ret,结果是1。
剩下的流程同第二步,可以自己理解一下。
如果再在代码后面加上下面两句:
程序会报错,告诉我们生成器里面没有东西了。程序运行完最后一个yield,再调用next( )程序会报错。
同样,把函数改成generator后,基本不用next( )方法来调用它,而是直接使用for循环来迭代。
总结:
函数中包含yield关键字, 返回的是生成器对象。
当第一次调用next(genObj), 才执行函数内容.
遇到yield关键字, 执行停止。
再次调用next方法时, 从上词停止的代码位置继续执行。
遇到yield关键字, 执行停止。
3.send方法
send()和next()一样,可以让生成器执行到下一个yield。
send()和next()的区别:
send可以给上一个yield的位置传递值,不能给最后一个yield传递值。在第一次执行生成器代码时,不能使用send()。
def fun():
while True:
print('welcome......')
receive = yield 'hello'
print(receive)
# f是生成器
f = fun()
in_function = next(f)
print("函数里面返回的值:", in_function)
f.send("西安邮电大学")
执行效果如下:
来分析一下执行过程:
-第一步:执行fun()会得到一个生成器,赋值给f。用next()方法调用f这个生成器,执行print()函数,打印‘welcome…’,因为没有遇到yield关键字,继续执行第二行代码,遇到yield停止,yield后面的值‘hello’返回给in_function。
-第二步:执行print()函数,打印“函数里面的值:hello”
-第三步:执行send(),将send里面的值‘西安邮电大学’赋值给上一个yield关键字的receive变量,因为没有遇到下一个yield关键字,因此继续执行print()函数,打印‘西安邮电大学’,继续执行,因为是死循环,所以执行print()函数,打印‘welcome…’,直到遇到yield停止。后面因为没有next()或者send()方法调用这个生成器,生成器不执行。
def grep(kw):
"""搜索关键字"""
while True:
response = ''
request = yield response
if kw in request:
print(request)
if __name__ == '__main__':
grep_gen = grep('python')
next(grep_gen)
# send方法可以给生成器传递数据, 并一直执行, 遇到yield停止。
grep_gen.send('I love python')
grep_gen.send('I love Java')