生成器 yield

由于生成器的其中一种创建方式与列表推导式很相似,这里先说一下列表推导式。

列表推导式

列表推导式又叫列表生成式,官方叫做 list comprehension。顾名思义,这个是用来生成列表的。

用法:

[x for x in iterable]

一般情况下,可以用list()函数将序列转换成列表,比如:

>>> list(range(1, 11))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

上面的情况比较单一,如果复杂一点,比如要生成[1x1, 2x2, 3x3, ..., 10x10],这个时候就只能循环了,列表推导式就是用来简化这种情况的。

>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

把要生成的元素x * x放在前面,后面跟for循环,就可以把列表创建出来。运用列表推导式,可以写出非常简洁的代码。

>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']
生成器表达式

通过列表推导式,可以直接生成一个列表,但是内存是有限的,如果一个列表太大,或者只需要访问列表中的部分元素,那其余大部分元素占用的空间就浪费了,生成器就是用来解决这个问题的。

生成器在访问序列中的元素时,不是一下加载整个序列到内存,而是一边循环一边计算,也就是读取一个元素计算一次,这样如果序列很大就节省了大量空间。说白了,生成器就是一种特殊的迭代器。

生成器有两种创建方法,第一种与列表推导式类似,只需要把列表推导式中的方括号[]改成圆括号()即可创建一个生成器(generator)。

>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000000001E74CA8>

>>> for i in g:
... 	print(i)
... 
0
1
4
9
16
25
36
49
64
81

创建生成器的第二种方法,是通过生成器函数。所谓生成器函数,就是在一个函数中包含yield语句。

比如斐波那契函数:

>>> def fib(max):
        n, a, b = 0, 0, 1
        while n < max:
            yield b
            a, b = b, a + b
            n = n + 1
        return 'done'
... ... ... ... ... ... ...

带有 yield 的函数不再是一个普通函数,而是一个generator。

>>> fib(10)
<generator object fib at 0x0000000001DE6A40>

>>> list(fib(10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

需要注意的是,生成器函数和普通函数执行的流程是不一样的。普通函数是遇到 return 语句或者执行结束就返回。而生成器函数是遇到 yield 就返回一个迭代值,再次执行时从上次返回的 yield 语句处继续执行,看起来就像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前迭代值。

好像说的有点绕,简单来说,yield就是中断函数并返回迭代值,而且中断不会对函数变量造成影响即状态不变,返回迭代值之后继续执行函数。

再看看下面这个示例:

>>> def odd():
        print('step 1')
        yield 1
        print('step 2')
        yield(3)
        print('step 3')
        yield(5)

>>> odd()
<generator object odd at 0x0000000001DE6A98>

>>> for i in odd():
... 	print(i)
... 
step 1
1
step 2
3
step 3
5

遇到yield就返回迭代值,这个返回类似return但不会退出函数(可以理解为挂起),然后继续执行函数,直到再次遇到yield。

另外,上面生成器函数fib()最后的retrun的值没有返回(在普通函数中是可以返回的)。因为,调用生成器函数并没有执行函数代码,而是返回了一个generator object,然后再从generator object中迭代取值的时候才会真正执行函数代码,所以调用生成器函数的时候并没有报异常并退出,return的值也没有返回。

在生成器中,无论迭代有没有完成,遇到return语句都会直接终止迭代并抛出StopIteration异常。。

这里顺便说一下迭代器,迭代器可以通过内置函数next()获取迭代器中的下一个元素,迭代完成时会报StopIteration异常。

>>> g = (x * x for x in range(5))
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

在 for 循环里,无需处理 StopIteration 异常,循环会正常结束。但是在while循环中会抛出异常,这时,需要捕获异常才会继续执行return语句,return的返回值包含在StopIteration的value中。

>>> g=fib(6)
>>> while True:
        x = next(g)
        print('g:', x)
... ... ... 
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
StopIteration: done

再来看上面的fib()函数的StopIteration异常捕获:

>>> g = fib(6)
>>> while True:
        try:
            x = next(g)
            print('g:', x)
        except StopIteration as e:
            print('Generator return value:', e.value)
            break
... ... ... ... ... ... ... 
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done

另一个示例,文件读取:

如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。

如果读取的文件很大,yield会分批返回,有效地避免了内存占用问题,轻松实现文件读取。

>>> def read_file(fpath): 
        BLOCK_SIZE = 1024 
        with open(fpath, 'rb') as f: 
            while True: 
                block = f.read(BLOCK_SIZE) 
                if block: 
                    yield block 
                else: 
                    return

>>> g = read_file('users.txt')
>>> for i in g:
... 	print(i)
... 
b'keith1:18:110\r\nkeith2:19:111\r\nkeith3:20:112\r\nkeith4:21:113'

参考:
https://docs.python.org/3/library/stdtypes.html#lists
https://docs.python.org/3/reference/simple_stmts.html#the-yield-statement
https://docs.python.org/3/reference/expressions.html#yieldexpr

posted @ 2017-10-28 17:49  KeithTt  阅读(229)  评论(0编辑  收藏  举报