python语言的产生_Python生成器是什么(超级详细)

之前我们讨论了高效的推导式。通过推导式,我们可以直接创建一个列表、字典或集合。但是,由于受到内存的限制,这些可迭代对象(列表、字典或集合)的容量是有限的。

比如,创建一个包含 10 万个元素的列表,不仅要占用很大的存储空间,而且根据局部性原理,在一段时间内我们要访问的仅仅局限于相邻的若干个元素,即使把所有元素都加载到内存之中,它们被“临幸”的概率也非常小。因此,大部分的存储空间其实是被白白浪费了。

基于此,我们就会有这样的需求:这些元素能不能按照某种算法推算出来,然后在后续循环过程中,根据这些元素不断推算出其他被访问的元素呢?这样一来,就不必创建完整的列表、字典或集合了,从而节省了大量的空间。在 Python 语言中,这种一边循环一边计算的机制,称为生成器。

Python生成器的定义

创建一个生成器并不复杂,方法也有很多。最简单的一种方法莫过于把一个列表推导式最外层的标记方括号[ ]改成圆括号( ),这样一个生成器就创建好了,示例代码如下。

In [1]: n = 10

In [2]: a = [x**2 for x in range (n) if x%2 == 0] #这是一个列表推导式

In [3]: print(a) #可正常输出

[0, 4, 16, 36, 64]

In [4]: type(a) #验明正身

Out[4]: list

In [5]: b = (x**2 for x in range(n) if x % 2==0) #这是一个生成器

In [6]: print(b) #无法直接输出

at 0xl07c88d00〉

In [7]: type(b) #验明正身

Out[7]: generator

上述代码的 In [2] 处是一个标准的列表生成式,一旦执行,就会把符合条件的列表元素全部加载到内存之中,此处生成的元素个数仅为 1 0个。但如果 n 为 100 万呢?列表 a 就会生成同样数量级别的元素,这无疑会浪费大量内存。

而在输入 In [5] 处,我们将 In [2] 处的最外层方括号[ ]替换为圆括号( ),这时它的类型就截然不同了。从 In [4] 和 In [7] 处输出的对象类型可以看出,前者 a 是一个列表,而后者 b 则是一个生成器。

在本质上,生成器就是一个生成元素的函数。现在你应该明白 In [5] 处最外层的那对圆括号( )的意义了吧,它不是“元组”生成式的标志,而更像是某个函数的标志(函数最核心的标志之一就是那对括号)。我们把这种表达式叫作生成器表达式(generator expression)

列表中的元素可以直接利用 print( ) 语句输出(如上述代码 In [6] 处),但同样的办法对生成器而言却是不可行的,解释器仅能给出生成器的地址信息。那么,该如何输出生成器中的每一个元素呢?这时,就需要借助全局内置函数 next( ),获得生成器的下一个返回值。

next( ) 函数好像拥有记忆一般,每使用一次 next( ) 函数就会顺序输出生成器的下一个元素,而不是从最开始的位置输出,直到输出最后一个元素,没有元素可输出时,就会抛出 StopIteration 异常。

In [8]: next(b)

Out[8]: 0

In [9]: next(b)

Out[9]: 4

In [10]: next(b)

Out[10]: 16

In [11]: b.__next__() #或用对象a的内部函数 __next__()来访问下一个元素

Out[11]: 36

...

由于生成器也是一个特殊的迭代器,所以它也会有内置函数 __next__(),在输入 In [11] 处,我们调用了它的内置函数 __next__(),也实现了和全局函数 next( ) 相同的效果。当我们不断执行 next(a) 时,它会不断输出 a 的下一个元素,直到没有更多的元素输出时,它会抛出 StopIteration 异常。

通常,生成器的正确打开方式并不是“傻乎乎”地反复调用 next( ) 函数,而是和循环(如 for、while 等)配套使用,由于 Python 语法糖会为我们保驾护航,确保访问不会越界,因此不会发生 StopIteration 异常,代码如下所示:

In [12]: a = (x**2 for x in range (n) if x%2 == 0) #此处 n = 10

In [13]: for num in a:

...: print(num)

...:

0

4

16

36

64

Python利用yield创建生成器

生成器的功能很强大。如果推算的算法比较复杂,难以利用列表推导式来生成,这时就可以使用含有 yield 关键字的函数。下面举例说明。

例如,在著名的斐波那契数列(Fibonacci)中,除第一个数和第二个数都为 1 之外,任意后面一个数都可由前两个数相加得到。

1,1, 2, 3, 5, 8, 13, 21, 34,...

分别以斐波那契数列中的元素为半径画出 1/4 圆,这些 1/4 圆连接起来的曲线称为斐波那契螺旋线,也称“黄金螺旋”,如图 1 所示。很神奇的是,在自然界中,很多生物(如向日葵、仙人掌、海螺等)中都存在斐波那契螺旋线的图案。

3-200512145F2561.gif

图 1:斐波那契螺旋线

回到关于生成器的讨论上来。生成斐波那契数列的过程相对比较复杂,难以利用列表推导式简练地表达出来,但可以用一个多行的函数描述出来,参见例 1。

【例 1】生成斐波那契数列的函数(fibonacci.py)

def fibonacci(xterms):

n, a, b = 0, 0, 1 #变量初始化

while n < xterms:

print(b, end = ' ')

a, b = b, a + b #变量更新

n = n + 1

return '输出完毕'

fibonacci(10)

程序执行结果为:

1 1 2 3 5 8 13 21 34 55

第 02 行和第 05 行代码体现了 Python 的特色—多变量赋值。

第 02 行代码的功能是对三个变量进行初始化赋值,它等价于如下代码:

n = 0

a = 0

b = 1

第 05 行代码的功能是循环更新变量值,它等价于如下代码:

a = b

b = a + b

由上面的代码可以看出,使用多变量赋值可以大大简化代码。但实际上,如前讨论,第 02 行和第 05 行实现的就是两个匿名元组之间的赋值。print( ) 默认的输出终结符是换行符,这里为了不占用打印空间,改成了空格(通过设置print()函数中的参数 end = ' '来实现),因此所有元素输出以空格隔开。

仔细观察可以看出,实际上,fibonacci( ) 函数中第 05 行代码已经清楚地定义了斐波那契数列的推算规则,我们可以从第一个元素开始,推算出后续任意元素。而这种推导逻辑已经非常接近生成器。也就是说,把上述函数稍加改造,就能把fibonacci()函数变成生成器:只需要把向屏幕输出的 print(b) 改为专用的 yield b 就大功告成了。参见例 2。

【例 2】生成斐波那契数列的生成器(fibonacci-gen.py)

def fibonacci(xterms):

n, a, b = 0, 0, 1

while n < xterms:

yield b #表明这是一个生成器

a, b = b, a + b

n = n + 1

return '输出完毕'

例 1 与例 2 的核心区别在于第 04 行,例 2 的第 04 行使用了关键字“yield”,这个关键字的本意就是“生产、产出”,如果某个函数定义中包含 yield 关键字,那么这个函数就不一般了,它不再是一个普通函数,而是一个生成器。

将上述函数加载到内存中以后,我们可以用如下代码来进行测试:

In [1]: func = fibonacci (10)

In [2] : func #并不直接输出

Out[2]:

通过前面的讨论,我们知道,In [1] 处的代码并不会执行 fibonacci( ) 函数,而是返回一个可迭代对象!这个对象并不能直接输出(见 In [2] 处),那该如何正确输出我们想要的结果呢?

第一种方法就是前面提到的反复利用 next( ) 函数,代码如下:

In [3]: next(func)

Out[3]: 1

In [4]: next(func)

Out[4]: 1

In [5]: next(func)

Out[5]: 2

In [6]: next(func)

Out[6]: 3

...

通过 next( ) 不断返回数列的下一个数,内存占用始终为常数。这是与列表推导式的显著不同。显然,如果生成器中“蕴涵”的数据较大,每次手动输入一个 next(func),才输出一个数据,麻烦至极。

因此,第二种方法更为常见,那就是和循环结构配套使用。我们重新加载 In [1] 处的代码并再次运行如下代码:

ln [7]: for item in func:

print(item, end = ' ')

程序执行结果为:

1 1 2 3 5 8 13 21 34 55

前面的几个生成器的案例其实并不实用,生成器的最佳应用场景在于:我们不想将所有计算出来的大量结果一块保存到内存之中。因为这样做会浪费大量不必要的内存资源。例如,将上面代码 In[1] 处的 10 改成 1000000,这时生成器的优势就体现出来了。因为生成器会“临时抱佛脚”,需要谁,就按照规则“临时”生成谁,它就好比是一个“经济适用房”,占用空间不大,但能解决实际问题。

Python生成器的执行流程

在这里,需要特别注意的是,生成器和函数的执行流程不一样。普通函数遇到 return 语句或者执行到最后一行函数语句时就会返回,结束整个函数的运行。

而变成生成器的函数,在每次调用 next( ) 的时候执行,遇到 yield 语句就“半途而废”,再次执行时,就会从上次返回的 yield 语句处接着往下执行。

下面列举一个简单的例子说明生成器的执行流程,见例 3。

【例 3】生成器的执行流程(my_gen.py)

def my_gen():

print('我是第1次返回')

yield(1)

print ('我是第2次返回')

yield(2)

print('我是第3次返回')

yield(3)

由于上述函数中含有 yield 语句,很显然,这是一个生成器。将上述函数加载到内存中之后, 我们来调用这个生成器。在调用生成器之前,首先要生成一个生成器对象,然后用 next( ) 函数不断获得下一个返回值,在 IPython 中的验证代码如下:

In [1]: gen = my_gen() #创建生成器对象

In [2]: next(gen) #输出生成器第一个元素,即第一个yield语句运行结果,并返回

我是第1次返回

Out[2]: 1

In [3]: next(gen) #从例 1 的第 04 行开始执行

我是第2次返回

Out [3] : 2

In [4]: next(gen) #从例 1 的第06行开始执行

我是第3次返回

Out[4]: 3

In [5]: next(gen) #无匹配的yield语句运行结果,发生异常,报错!

-----------------------------------------------------------------------

Stopiteration Traceback (most recent call last)

in

-----> 1 next(gen)

Stopiteration:

总结一下,在本质上,生成器就是一种元素生成函数,它和普通函数的不同之处在于,它的返回值不是通过 return 返回的,而是通过 yield 返回的。

另外一个需要注意的地方是,含有 yield 语句的函数中如果还配有return语句,那么这个 return 语句并不是用于函数正常返回的,而是 StopIteration 的异常说明。也就是说,生成器没有办法使用 return 的返回值。如果想获得该返回值,需要捕获 StopIteration 异常,然后输出 StopIteration.value。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值