【Python】详解 生成器 (generator) —— 迭代器完全解读(下)


目录

一、绪论

二、生成器 (generator)

2.1 说明

2.2 生成器函数

2.2.1 原理概述

2.2.2 yield 表达式

2.3 生成器表达式


一、绪论

        在《【Python】详解 可迭代对象 (iterable) 与 迭代器 (iterator) —— 迭代器完全解读(上) 》一文中,详细解读了相关概念及其原理,并文末提到了另一种迭代器 —— 生成器 (generator)。事实上,在诸如 C、C++、JAVA 等主流语言中,并无生成器的概念。而在 Python 中,生成器则与迭代器相辅相成 —— 迭代器 是 生成一个遍历数据的迭代工具,而 生成器 则是 一个数据生成工具

        举个典例:已知,斐波那契数列从第三个数开始等于前两数之和。假设想获取 1024 万个斐波那契数列,按传统方法需开辟一个长度为 1024 万的数组,再按照斐波那契数列定义逐个计算 —— 显然内存限制下,不可能同时创建如此多元素。即便可以创建,也会占用大量存储空间。而且倘若仅需访问其中几个元素,那么大部分元素的空间都浪费了。

        那么,可否像迭代器那样构建一个生成数据的方法,每次调用才获取下一个结果,而不是将所有结果都存在内存中呢?这就是 生成器 —— 一种 惰性序列,需要多少就调用多少次 —— 按需生成,以从根本上解决存储问题


二、生成器 (generator)

2.1 说明

        生成器 (generator) 用于 返回 生成器迭代器 (generator iterator)。已知:

  • 可迭代对象 (iterable):支持 迭代协议 (实现了 __iter__() 方法)
  • 迭代器 (iterator):支持 迭代器协议 (实现了 __iter__() 和 __next__() 方法)

        生成器 无需显式定义 __iter__() 和__next__() 方法,因为实例化生成器对象后将 自动支持迭代器协议,故 生成器也是一种迭代器,是实现迭代器协议的便捷方式,写法更为紧凑。

        通常,使用两种方式创建生成器:生成器函数 生成 (器) 表达式。(生成器通常默认指前者)


2.2 生成器函数

2.2.1 原理概述

        生成器函数 用于创建 生成器迭代器 (generator iterator)

        与普通函数的 区别 在于:普通函数 使用 return 返回结果,而 生成器函数 使用 yield 返回结果。

        使用了 yield 表达式 的生成器函数 返回一个迭代器对象,以便于通过 for 循环或 next() 函数等逐项获取元素/数据/值/成员 (以下统称 元素)。换言之,使用了 yield 表达式的函数就是生成器函数

        更具体地,在调用生成器函数时,每次遇到 yield 表达式,生成器函数就会 暂停并保存当前位置的执行状态 (包括局部变量和挂起的 try 语句),然后 返回 yield 表达式的值,并在 下一次对生成器函数执行 next() 方法 时,从上次离开的位置继续执行 (有别于每次调用普通函数都重新从头开始)。

        例如,生成斐波那契数列前 max 个数,使用 普通函数 

>>> def fib_common(max):
	a, b, counter = 0, 1, 0
	while counter < max:
		print(a)
		a, b = b, a + b
		counter += 1
	return

>>> fib_common(8)  # max=8
0 
1 
1 
2 
3 
5 
8 
13 

        此时,将 print(a) 改成 yield a 即得 生成器函数

>>> def fib_gen(max):
	a, b, counter = 0, 1, 0
	while counter < max:
		yield a  # fib_gen() 因此变为生成器函数
		a, b = b, a + b
		counter += 1
	return

        定义一个 生成器对象 fg,检验其性质:

>>> fg = fib_gen(8)  # max=8
>>> fg
<generator object fib_gen at 0x0000020E3F704780>

>>> from collections import Iterable, Iterator, Generator  # 导入抽象基类检验
>>> isinstance(fg, Iterable), isinstance(fg, Iterator), isinstance(fg, Generator)  # 可见生成器对象也是一种可迭代对象
(True, True, True)

        可见,含有 yield 表达式 生成器函数,其实例 是生成器 (generator),也是迭代器 (iterator),更为可迭代对象 (iterable)。这时,要想迭代生成器的各项元素,就必须 显式或隐式地调用 next() 函数,一个一个地计算结果:

>>> import sys
>>> while True:  
        try:
            print(next(fg))  # 显式调用 next() 获取生成器对象的元素
        except StopIteration:
            sys.exit()
        
0
1
1
2
3
5
8
13

2.2.2 yield 表达式

        上节并未直观地展现 yield 表达式 的原理,因而由本节举例说明。自定义一个生成器函数:

>>> def num_gen():  # 因为包含 yield 表达式, 所以该函数是生成器函数
	index = 0
	print('index=', index)
	yield 0

	index += 1
	print('index=', index)
	yield 1

	index += 1
	print('index=', index)
	yield 2

>>> hasattr(num_gen, '__next__')  # 生成器函数 num_gen() 并未显式支持迭代器协议
False

        调用该生成器函数时,需要先实例化一个 生成器 (迭代器) 对象

>>> ng = num_gen()
>>> hasattr(ng, '__iter__'), hasattr(ng, '__next__')  # 实例化后的对象自动支持迭代器协议, 而无需显式定义
(True, True)                                          # 可见生成器也是迭代器 (自然也是可迭代对象)

        可见,生成器函数的实例对象 自动支持迭代器协议 (自动创建 __iter__() 和 __next__() 方法)。再使用 next() 函数或调用 __next__() 方法依次获取下一项:

>>> next(ng)
index=0
0
>>> ng.__next__()  # 可见每次重新调用, 都会从上一次执行中断的位置状态继续
index=1
1
>>> next(ng)  # index 的变化就证明了上述结论
index=2
2
>>> next(ng)  # 生成器作为迭代器, 同样会因为迭代元素的耗尽而引发 StopIteration 异常
Traceback (most recent call last):
  File "<pyshell#126>", line 1, in <module>
    next(ng)
StopIteration

        根据 print() 函数的打印结果可知,函数每次遇到 yield 就返回 yield 表达值中的值 (和 return 一样会将 yield 后的内容返回给调用方),然后中断执行退出函数,保存当前位置状态,并在下一次调用 next() 时恢复所保存的位置状态,继续往下执行。可以说,每次调用 next() 都相当于执行一个 代码块,而代码块之间的是由 yield 表达式分隔的。生成器作为迭代器,同样会因为迭代元素的耗尽而引发 StopIteration 异常,以避免无限迭代。

        注意,上述解读提到了 每次 yield 中断执行都会保存当前位置状态,而 num_gen() 函数的局部变量 index 状态变化就证明了这一点。而若以 return 取代 yield 则会变回普通函数,那么每次调用函数都不会保存前向调用的状态,而是重新从头初始化状态。

        此外,还有一种 yield from 表达式 可以返回一个迭代器或生成器执行 next 后的结果,暂不赘述。


2.3 生成器表达式

        事实上,除了用生成器函数来创建生成器,还可以通过另一种简洁的方式 —— 生成器表达式

        形式上,生成器表达式类似于列表推导式,但使用的是 圆括号 () 而非方括号 [];结果上,生成器表达式不会像列表推导式一样一次性返回所有计算结果,而是通过 延迟计算按需生成。可以说,生成器表达式结合了迭代功能和解析功能。

>>> [i for i in range(4)]  # 列表推导式 / 列表解析式
[0, 1, 2, 3]  # 返回完整列表对象

>>> (j for j in range(4))  # 生成器表达式
<generator object <genexpr> at 0x0000020E3F704888>  # 返回一个生成器对象

        生成器表达式可认为是一种特殊的生成器函数,类似于 lambda 表达式之于普通函数的关系。虽然形式上更简洁、紧凑,但生成器表达式同样能够返回一个生成器(迭代器)。

        因为生成器也是迭代器、可迭代对象,所以适用于后者的函数同样适用于前者并常配合使用,例如:

>>> sum(j for j in range(4))  # sum(iterable, start=0, /)
6
>>> min(j for j in range(4))  # min(iterable, *[, default=obj, key=func])
0 
>>> max(j for j in range(4))  # max(iterable, *[, default=obj, key=func])
3
# -----------------------------------------------------------------------------------
>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> list(i+j for i, j in zip(x, y))  # zip(iter1 [,iter2 [...]])
[5, 7, 9]

        事实上,生成器表达器比列表推导式更高效,例如:

>>> sum((j for j in range(4)))  # 为方便, 也可省略生成器表达式的内层圆括号
6

>>> sum([j for j in range(4)])  # 列表推导式写法
6

        上述两个例子中,生成器表达式一边生成元素一边求和,而 列表推导式却先计算出一个完整列表再求和,从而后者多出的操作引起了时间效率的 落后

        此外,要逐个输出元素,需要先实例化生成器对象(使用变量保存生成器对象的引用/指向生成器对象),再使用 __next__() 方法 或 next() 函数获取下一项元素,例如:

>>> str_gen = (s for s in 'abc')
>>> str_gen
<generator object <genexpr> at 0x0000020E3F704888>

>>> str_gen.__next__()
'a'
>>> next(str_gen)
'b'
>>> str_gen.__next__()
'c'
>>> next(str_gen)
Traceback (most recent call last):
  File "<pyshell#167>", line 1, in <module>
    next(str_gen)
StopIteration

        总而言之,生成器不会将所有的元素一次性加载到内存中,而是 延迟计算,一次计算一个结果。

        生成器的 优点 是:代码简洁紧凑、可读性好、时效较高等;缺点 在于:只能依次往下迭代一轮,而不能回退和重复迭代。


参考文献:

术语对照表 — Python 3.6.15 文档

7. 简单语句 — Python 3.6.15 文档

Python3 迭代器与生成器 | 菜鸟教程

生成器 - 廖雪峰的官方网站

https://zhuanlan.zhihu.com/p/76831058

6. 表达式 — Python 3.6.15 文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值