Python之生成器generator
生成器:只有在调用的时候才会生成相应的数据(调用到这个数据的时候才会生成这个数据,没有调用到时就没有这个数据)
只记录数据的当前位置
生成器不能像普通的列表一样,通过下标或者切片的方式去取
生成器只能通过 循环 或者__next__()(2.x中用next())方法去取。
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。
而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。
在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个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 0x1022ef630>
创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator。
我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?
正确的方法是使用for循环,因为generator也是可迭代对象:
>>> g = (x * x for x in range(10)) >>> for n in g: ... print(n) ... 0 1 4 9 16 25 36 49 64 81
generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。
比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
def fib(max): n, a, b = 0, 0, 1 while n < max: print(b) a, b = b, a + b n = n + 1 return 'done'
上面的函数可以输出斐波那契数列的前N个数:
>>> fib(6) 1 1 2 3 5 8 'done'
仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。
也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator,只需要把print(b)改为yield b就可以了:
def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1 return 'done'
这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:
>>> f = fib(6) >>> f <generator object fib at 0x104feaaa0>
这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
举个简单的例子,定义一个generator,依次返回数字1,3,5:
def odd(): print('step 1') yield 1 print('step 2') yield(3) print('step 3') yield(5)
生成器表达式样例
通过改进查找文件中最长的行的功能实现来看看生成器的优势。
EXAMPLE 4 : 一个比较通常的方法,通过循环将更长的行赋值给变量 longest 。
f = open('FILENAME', 'r') longest = 0 while True: linelen = len(f.readline().strip()) if not linelen: break if linelen > longest: longest = linelen f.close() return longest
很明显的,在这里例子中,需要迭代的对象是一个文件对象。
改进 1:
需要注意的是,如果我们读取一个文件所有的行,那么我们应该尽早的去释放这个文件资源。
例如:一个日志文件,会有很多不同的进程会其进行操作,所以我们不能容忍任意一个进程拿着这个文件的句柄不放。
f = open('FILENAME', 'r') longest = 0 allLines = f.readlines() f.close() for line in allLines: linelen = len(line.strip()) if not linelen: break if linelen > longest: longest = linelen return longest
改进 2:
我们可以使用列表解析来简化上述的代码,例如:在得到 allLines 所有行的列表时对每一行都进行处理。
f = open('FILENAME', 'r') longest = 0 allLines = [x.strip() for x in f.readlines()] f.close() for line in allLines: linelen = len(line) if not linelen: break if linelen > longest: longest = linelen return longest
改进 3:
当我们处理一个巨大的文件时,file.readlines() 并不是一个明智的选择,因为 readlines() 会读取文件中所有的行。
那么我们是否有别的方法来获取所有行的列表呢?我们可以应用 file 文件内置的迭代器。
f = open('FILENAME', 'r') allLinesLen = [line(x.strip()) for x in f] f.close() return max(allLinesLen) # 返回列表中最大的数值
不再需要使用循环比较并保留当前最大值的方法来处理,将所有行的长度最后元素存放在列表对象中,再获取做大的值即可。
改进 4:
这里仍然存在一个问题,就是使用列表解析来处理 file 对象时,会将 file 所有的行都读取到内存中,然后再创建一个新的列表对象,
这是一个内存不友好的实现方式。那么,我们就可以使用生成器表达式来替代列表解析。
f = open('FILENAME', 'r') allLinesLen = (line(x.strip()) for x in f) # 这里的 x 相当于 yield x f.close() return max(allLinesLen)
因为如果在函数中使用生成器表达式作为参数时,我们可以忽略括号 ‘()’,所以还能够进一步简化代码:
f = open('FILENAME', 'r') longest = max(line(x.strip()) for x in f) f.close() return longest
最后:我们能够以一行代码实现这个功能,让 Python 解析器去处理打开的文件。
当然并不是说代码越少就越好,例如下面这一行代码每循环一次就会调用一个 open() 函数,效率上并没有 改进 4 更高。
return max(line(x.strip()) for x in open('FILENAME'))
小结
在需要迭代穿越一个对象时,我们应该优先考虑使用生成器替代迭代器,使用生成器表达式替代列表解析。当然这并不是绝对的。 迭代器和生成器是 Python 很重要的特性,对其有很好的理解能够写出更加 Pythonic 的代码。

浙公网安备 33010602011771号