Loading

Python - 生成器

before

本篇来学习一种特殊的迭代器——生成器。

生成器函数

先上例子:

from collections import Iterator  
def generator_func():  
    print('first')  
    yield 1  
    print('second')  
    yield 2  
    print('third')  
    yield 3
generator_obj = generator_func()  
print(isinstance(generator_obj, Iterator))  # True  
print(generator_obj)                        # <generator object generator_func at 0x00D25AB0>  
print(generator_obj.__next__())             # first     1  
print(generator_obj.__next__())             # second    2  
print(generator_obj.__next__())             # third     3  
print(generator_obj.__next__())             # StopIteration   

在上例中,第2行,定义generator_func函数,在第3到8行我们做了打印和yield操作,第9行我们执行函数并拿到返回值并赋值给变量generator_obj。通过第10行可以看到generator_obj是迭代器,也就是说generator_func函数返回的是迭代器。第11行打印这个返回值,可以看到是个生成器(generator)对象,既然generator_obj是迭代器,我们通过第12到15行的打印印证了这一点——迭代器有__next__方法,并且第15行也报了StopIteration错误。那么函数内的yield是什么呢?迭代器在执行第12行__next__方法的时候,分别打印了first(第3行的print)和1,这个1就是yield返回的,而通过下面的打印和yield可以看到,每当我们执行一次__next__方法,就执行了一次打印和yield,直到第15行再次执行__next__方法时,报了StopIteration的错误。通过上例,我们可以总结。

函数体内包含有yield关键字,那么该函数被称为生成器函数,而该函数执行的结果(返回值generator_obj)为生成器。我们可以通过下面示例来证明:

from collections import Generator

def foo():
    return 1
def bar():
    yield 1

f = foo()
b = bar()

print(isinstance(f, Generator))  # False
print(type(f))  # <class 'int'>
print(f)  # 1
print(isinstance(b, Generator))  # True
print(b)  # <generator object bar at 0x000001B7A5EEBF68>

来看看yield的功能:

  • yield与return一样可以终止函数执行、可以返回值(不指定返回值默认返回None),但不同之处yield可以在函数内多次使用,而return只能返回一次。

  • 为函数封装好了__iter____next__方法,把函数执行结果转换为迭代器,也就是说yield自动实现了迭代协议并遵循迭代器协议。

  • 触发函数执行、暂停、再继续,包括状态都由yield保存。

  • 生成器本质就是迭代器。

  • 延迟计算,每调用一次,yield返回一次,并保存此次调用的相关信息,等待下一次调用。

生成器函数和普通的函数最大的不同之处在于,生成器每当yield一次,在返回值的时候,将函数挂起,保存相关信息,在下一次函数执行的时候,从当前挂起的位置继续执行。

def generator_func():  
    print('first')  
    yield 1  
    print('second')  
    yield 2  
    print('third')  
    yield 3  
generator_obj = generator_func()  
print(generator_obj.__next__())  
print(''.center(20, '*'))  
for i in generator_obj:  
    print(i)  
''' 
    first 
    1 
    ******************** 
    second 
    2 
    third 
    3 
'''  

由上例看到,在第9行执行触发了第3行的yield之后,在第11行的循环中,是从第4行开始执行的循环,由此证明,yield“记住”了在第9行时的信息,当第11行再次触发函数的执行时,yield在保存的信息中,找到上一次执行的状态并恢复,所以函数从第4行继续执行的。

yield也可以返回任意对象:

def gen():  
    def foo():  
        print('foo function')  
    yield 1, 3, foo  
g = gen()  
g1 = g.__next__()  
g1[2]()     							# foo function  

yield在返回值方面与return一致。多个值以元组的方式返回:

def generator_func():  
    for i in range(1, 5):  
        x = yield 'yield: %s ' % i  
        print('x =', x)  
generator_obj = generator_func()  
print(generator_obj.__next__())  
print(generator_obj.send('test'))  
print(generator_obj.__next__())  
print(generator_obj.close())  
''' 
    yield: 1  
    x = test 
    yield: 2  
    x = None 
    yield: 3  
    None 
'''  

上例中,我们通过send方法为yield下次调用传递值,而close方法关闭生成器。

需要注意的是,x = yield是调用者为生成器传值,而yield i是生成器为调用者返回值。

生成器表达式VS列表解析式

说了那么多,这生成器有什么用呢?还记得开发把10万+的函数名放在一个列表内的故事吗?现在需求是这样的,这10万+的列表,每次只需要前5个函数名去执行测试任务,而且,取出来的同时从列表内删除。怎么做呢?我们用列表解析式快速创建这个10万+的列表,然后在取值的同时执行删除操作,是不是很麻烦,而且,10万+的列表放在内存中是不是很占内从空间?那么此时,用生成器就很好地解决了这个问题。

首先学习一个知识点,生成器表达式。

l = [i for i in range(10)]  
g = (i for i in range(10))  
print('l: ', l) 	# l:  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]  
print('g: ', g) 	# g:  <generator object <genexpr> at 0x00DE5AB0>  

由上例,可以看到,生成器表达式和列表解析式类似,只是由中括号换成了小括号,但生成器表达式返回的是生成器对象。而列表解析式返回的是列表。那么二者有什么区别呢?

import time  
def timer(func):  
    def wrapper(*args, **kwargs):  
        start = time.time()  
        func(*args, **kwargs)  
        print('%s running time: %s' % (func.__name__, time.time() - start))  
    return wrapper  
@timer  
def gen():  
    i = (i for i in range(10000000))  
    print(i)  
@timer  
def li():  
    [i for i in range(10000000)]  
gen()  
li()  
''' 
    <generator object gen.<locals>.<genexpr> at 0x00A88CF0> 
    gen running time: 0.0 
    li running time: 0.7966420650482178 
'''  

上例中展示了分别用生成器表达式和列表解析创建一个一千万个元素的列表,从创建时间来说,生成器表达式完成的时间太快了(其实就返回了一个生成器),我们的timer时间装饰器没有测出时间,而列表解析则非常耗时,而这只是千万级,如果是亿级的列表,一般的电脑则会报MemoryError的错误。

这就体现出了生成器表达式的优势:

  • 节省内存。

  • 惰性计算,举个例子,老男孩向列表解析式制衣厂和生成器表达式制衣厂分别定做两万件校服,但没说什么时候取。列表解析式制衣厂拿到订单就把两万件校服做出来了,但由于老男孩没有及时取,就积压在仓库里了。而生成器表达式制衣厂比较鸡贼,表面上答应了做两万件,但其实一件也没做,而老男孩因为种种原因,每家只需要20件校服,列表解析式制衣厂就尴尬了,一万多件校服白做了,而生成器表达式制衣厂则高效率的只做了20件校服,生成器表达式的优势就出来了,需要多少我(生成器)就给多少,多一件都不做。

send

生成器有个send方法可以在外部和生成器内部进行通信,比如从外部传值给生成器内部,这在一些特定情况下比较有用(这句话听听就得了,我暂时还没找到这种场景!)。

基本用法:

def func():
    print('first')
    result1 = yield 1
    print(result1, 'second')
    result2 = yield 2
    print(result2, 'third')
    yield 3

gen = func()  # 首先拿到一个生成器对象
print(gen.send(None))   # 1
print(gen.send('a'))    # 2
print(gen.send('b'))    # 3
print(gen.send('c'))  # StopIteration

"""
first
1
a second
2
b third
3
StopIteration
"""

上述生成器执行过程:

  1. gen.send(None)启动生成器,打印firstyield1返回;挂起;print打印返回值1
  2. gen.send('a')触发生成器执行, 从上一次挂起的地方往下执行,拿到send的值赋值给result1并打印a secondyield2返回;挂起;print打印返回值2
  3. gen.send('b')触发生成器执行, 从上一次挂起的地方往下执行,拿到send的值赋值给result2并打印b secondyield3返回;挂起;print打印返回值3
  4. gen.send('c')触发生成器执行,但yield 3后没有代码,意味着生成器为空,所以报错StopIteration

这里要注意的是,如果使用send启动生成器,必须send一个None值,否则报错TypeError: can't send non-None value to a just-started generator,另外,当send值为None时,它和next()方法是等价的。


欢迎斧正,that's all,see also:

python特性(八):生成器对象的send方法

posted @ 2020-11-10 10:32  听雨危楼  阅读(104)  评论(0编辑  收藏  举报