生成器 ==== 生成器(generator)也是一种迭代器,在每次迭代时返回一个值,直到抛出 `StopIteration` 异常。它有两种构造方式: - 生成器表达式 和列表推导式的定义类似,生成器表达式使用 `()` 而不是 `[]`,比如: ``` numbers = (x for x in range(5)) # 注意是(),而不是[] for num in numbers: print num ``` - 生成器函数 含有 `yield` 关键字的函数,调用该函数时会返回一个生成器。 本文主要讲**生成器函数**。 # 生成器函数 先来看一个简单的例子: ```python >>> def generator_function(): ... print 'hello 1' ... yield 1 ... print 'hello 2' ... yield 2 ... print 'hello 3' >>> >>> g = generator_function() # 函数没有立即执行,而是返回了一个生成器,当然也是一个迭代器 >>> g.next() # 当使用 next() 方法,或使用 next(g) 的时候开始执行,遇到 yield 暂停 hello 1 1 >>> g.next() # 从原来暂停的地方继续执行 hello 2 2 >>> g.next() # 从原来暂停的地方继续执行,没有 yield,抛出异常 hello 3 Traceback (most recent call last): File "", line 1, in StopIteration ``` 可以看到,上面的函数没有使用 `return` 语句返回值,而是使用了 `yield`『生出』一个值。一个带有 `yield` 的函数就是一个生成器函数,当我们使用 `yield` 时,它帮我们自动创建了 `__iter__()` 和 `next()` 方法,而且在没有数据时,也会抛出 `StopIteration` 异常,也就是我们不费吹灰之力就获得了一个迭代器,非常简洁和高效。 带有 `yield` 的函数执行过程比较特别: - 调用该函数的时候不会立即执行代码,而是返回了一个生成器对象; - 当使用 `next()` (在 for 循环中会自动调用 `next()`) 作用于返回的生成器对象时,函数开始执行,在遇到 `yield` 的时候会『暂停』,并返回当前的迭代值; - 当再次使用 `next()` 的时候,函数会从原来『暂停』的地方继续执行,直到遇到 `yield` 语句,如果没有 `yield` 语句,则抛出异常; 整个过程看起来就是不断地 `执行->中断->执行->中断` 的过程。一开始,调用生成器函数的时候,函数不会立即执行,而是返回一个生成器对象;然后,当我们使用 `next()` 作用于它的时候,它开始执行,遇到 `yield` 语句的时候,执行被中断,并返回当前的迭代值,要注意的是,此刻会记住中断的位置和所有的变量值,也就是执行时的上下文环境被保留起来;当再次使用 `next()` 的时候,从原来中断的地方继续执行,直至遇到 `yield`,如果没有 `yield`,则抛出异常。 **简而言之,就是 `next` 使函数执行,`yield` 使函数暂停。** # 例子 看一个 Fibonacci 数列的例子,如果使用自定义迭代器的方法,是这样的: ```python >>> class Fib(object): ... def __init__(self): ... self.a, self.b = 0, 1 ... def __iter__(self): ... return self ... def next(self): ... self.a, self.b = self.b, self.a + self.b ... return self.a ... >>> f = Fib() >>> for item in f: ... if item > 10: ... break ... print item ... 1 1 2 3 5 8 ``` 而使用生成器的方法,是这样的: ```python >>> def fib(): ... a, b = 0, 1 ... while True: ... a, b = b, a + b ... yield a ... >>> f = fib() >>> for item in f: ... if item > 10: ... break ... print item ... 1 1 2 3 5 8 ``` 可以看到,使用生成器的方法非常简洁,不用自定义 `__iter__()` 和 `next()` 方法。 另外,在处理大文件的时候,我们可能无法一次性将其载入内存,这时可以通过构造固定长度的缓冲区,来不断读取文件内容。有了 `yield`,我们就不用自己实现读文件的迭代器了,比如下面的实现: ```python def read_in_chunks(file_object, chunk_size=1024): """Lazy function (generator) to read a file piece by piece. Default chunk size: 1k.""" while True: data = file_object.read(chunk_size) if not data: break yield data f = open('really_big_file.dat') for piece in read_in_chunks(f): process_data(piece) ``` # 进阶使用 我们除了能对生成器进行迭代使它返回值外,还能: - 使用 `send()` 方法给它发送消息; - 使用 `thow()` 方法给它发送异常; - 使用 `close()` 方法关闭生成器; ## send() 方法 看一个简单的例子: ``` >>> def generator_function(): ... value1 = yield 0 ... print 'value1 is ', value1 ... value2 = yield 1 ... print 'value2 is ', value2 ... value3 = yield 2 ... print 'value3 is ', value3 ... >>> g = generator_function() >>> g.next() # 调用 next() 方法开始执行,返回 0 0 >>> g.send(2) value1 is 2 1 >>> g.send(3) value2 is 3 2 >>> g.send(4) value3 is 4 Traceback (most recent call last): File "", line 1, in StopIteration ``` 在上面的代码中,我们先调用 `next()` 方法,使函数开始执行,代码执行到 `yield 0` 的时候暂停,返回了 0;接着,我们执行了 `send()` 方法,它会恢复生成器的运行,并将发送的值赋给上次中断时 yield 表达式的执行结果,也就是 value1,这时控制台打印出 value1 的值,并继续执行,直到遇到 yield 后暂停,此时返回 1;类似地,再次执行 `send()` 方法,将值赋给 value2。 简单地说,`send()` 方法就是 `next()` 的功能,加上传值给 `yield`。 ## throw() 方法 除了可以给生成器传值,我们还可以给它传异常,比如: ```python >>> def generator_function(): ... try: ... yield 'Normal' ... except ValueError: ... yield 'Error' ... finally: ... print 'Finally' ... >>> g = generator_function() >>> g.next() 'Normal' >>> g.throw(ValueError) 'Error' >>> g.next() Finally Traceback (most recent call last): File "", line 1, in StopIteration ``` 可以看到,`throw()` 方法向生成器函数传递了 `ValueError` 异常,此时代码进入 except ValueError 语句,遇到 yield 'Error',暂停并返回 Error 字符串。 简单的说,`throw()` 就是 `next()` 的功能,加上传异常给 `yield`。 ## close() 方法 我们可以使用 `close()` 方法来关闭一个生成器。生成器被关闭后,再次调用 next() 方法,不管能否遇到 yield 关键字,都会抛出 StopIteration 异常,比如: ```python >>> def generator_function(): ... yield 1 ... yield 2 ... yield 3 ... >>> g = generator_function() >>> >>> g.next() 1 >>> g.close() # 关闭生成器 >>> g.next() Traceback (most recent call last): File "", line 1, in StopIteration ``` # 小结 - yield 把函数变成了一个生成器。 - 生成器函数的执行过程看起来就是不断地 `执行->中断->执行->中断` 的过程。 - 一开始,调用生成器函数的时候,函数不会立即执行,而是返回一个生成器对象; - 然后,当我们使用 `next()` 作用于它的时候,它开始执行,遇到 `yield` 语句的时候,执行被中断,并返回当前的迭代值,要注意的是,此刻会记住中断的位置和所有的数据,也就是执行时的上下文环境被保留起来; - 当再次使用 `next()` 的时候,从原来中断的地方继续执行,直至遇到 `yield`,如果没有 `yield`,则抛出异常。 # 参考资料 - [Python yield 使用浅析](https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/) - [谈谈Python的生成器 – 思诚之道](http://www.bjhee.com/python-yield.html) - [Function vs Generator in Python](https://code-maven.com/function-vs-generator-in-python) - [Lazy Method for Reading Big File in Python? - Stack Overflow](http://stackoverflow.com/questions/519633/lazy-method-for-reading-big-file-in-python)