第二十章 推导和生成

列表推导与函数是编程工具

列表推导vs map

加入我们有这样的一个需求,获取字符串中每个字符的ASCII编码,我们可以使用for循环和map函数实现:
使用for循环:

>>> res = []
>>> for x in 'spam':
...     res.append(ord(x))
... 
>>> res
[115, 112, 97, 109]

使用map函数:

>>> res = list(map(ord, 'spam'))
>>> res
[115, 112, 97, 109]

使用列表推导:

>>> res = [ord(x) for x in 'spam']
>>> res
[115, 112, 97, 109]

标准推导语法

列表推导最简单的形式:

[expression for target in iterable]

可以嵌套任意数量的for循环,并且每一个都有可选的关联的if测试:

[expression for target1 in iterable1 if condition1
			for target2 in iterable2 if condition2
			for target3 in iterable3 if condition3
			.
			.
			for targetN in iterableN if conditionN]

上面的列表推导可以写成下面形式的for循环:

for target1 in iterable1:
	if condition1:
		for target2 in iterable2:
			if condition2
				for target3 in iterable3:
					if condition3
						.
						.
						.
						for targetN in iterableN if conditionN]
							if conditionN

生成器函数与表达式

如今python对延时提供了更多的支持:它提供了在需要的时候才产生结果的工具,而不是立即产生结果。下面介绍的两种语言特性也能让用户定义的操作推迟结果的计算:

  • 生成器函数
  • 生成器表达式

由于两者都不会一次性地创建一个列表,它们节省了内存空间,并且允许计算时间分摊到各次结果请求上。

生成器函数:yield vs return

状态挂起

和返回一个值并退出的常规函数不同,生成器函数能够自动挂起并在生成值的时刻恢复之前的状态并继续函数的执行。由于生成器函数在挂起时保存的状态包含它们的代码位置和整个局部作用域,因此当函数恢复时,它们的局部变量保持了信息并且使其可用。生成器函数和常规函数之间主要的代码不同之处在于,生成器函数产生(yield)一个值,而不是返回(return)一个值。yield语句会挂起该函数并向调用者传回一个值,同时也保留的状态使函数能从离开的地方继续。

与迭代协议集成

要真正地理解生成器函数,你需要知道它们与 Python中的迭代协议的概念密切相关。正如我们已经看到的,迭代器对象定义了一个__next__方法,它要么返迭代中的下一项,要么引发一个特殊的 StopIteration异常来终止选代,一个可选代对象的选代器用iter内置函数来接收值,面对于自身就是选代器的可选代对象而言这一多骤是空操作。
如果支持该协议的话, Python的for循环以及其他的迭代上下文,会使用这种跌代协议来遍历一个序列或值生成器(如果不支持,选代则返回重复索引的序列)。任何支持这一接口的对象都能在所有的选代工具中工作。
为了支持这一协议,函数必须包含一条yield语句,该函数将被特别编译为生成器:它们不再是普通的函数,而是作为通过特定的跌代协议方法来返回对象的函数。当调用时,它们返回一个生成器对象,该对象支持用一个自动创建的名为next的方法接口,来开始或恢复执行。
生成器函数也可以有一条 return语句,不过总是出现在def语句块的末尾,用于终止值的生成:从技术上讲,可以在任何常规函数退出动作之后,引发一个 StopIteration异常来终止值的生成。从调用者的角度来看,生成器的__next__方法将恢复函数执行并且运行到下一个yield结果的传回或引发一个 StopIteration异常。
最终效果就是生成器函数,被编写为包含 yield语句的def语句,能自动地支持选代协议,并且可以用在任何选代上下文中以随着时间根据需要产生结果。

生成器函数的应用

>>> def gensquares(N):
...     for i in range(N):
...             yield i ** 2
... 
>>> gensquares(5)
<generator object gensquares at 0x108540450>
>>>
>>> g = gensquares(5)
>>> g.__next__()
0
>>> g.__next__()
1
>>> g.__next__()
4
>>> g.__next__()
9
>>> g.__next__()
16
>>> g.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

由于for循环可以自动的自动生成迭代器,并自动调用__next__方法,可以一次完成值的生成结果:

>>> for i in gensquares(5):
...     print(i, end=' : ')
... 
0 : 1 : 4 : 9 : 16 : >>> 

为什么要使用生成器函数?

生成器对于大型程序而言,在内存使用和性能方面都更好,它们允许函数避免预先做好所有的工作,在结果的列表很大或者在处理每一个结果都需要很多时间时,这一点尤其有用。生成器将产生一系列值的时间分散到每一次的循环选代中去。

扩展生成器函数协议:send vs next

send方法生成一系列结果的下一个元素,这一点就想__next__方法一样,但是它也提供了一种调用者与生成器之间进行通信的方式,从而能够影响生成器的操作;
从技术上来讲,yield现在是一个表达式的形式,而不再是一条语句,它会返回发送给send函数的元素。
当使用这一额外的协议时,值可以通过调用G.send(value)发送给一个生成器G。之后恢复生成器代码的执行,并且生成器中的yield表达式返回了发送给send函数的值。如果提前调用了正常的G. __next __()方法,yield则返回None,例如:

>>> def gen():
...     for i in range(10):
...             x = yield i
...             print(x)
... 
>>> G = gen()
>>> next(G)
0
>>> G.send(77)
77
1
>>> G.send(88)
88
2
>>> next(G)
None
3

生成器还支持一个throw(type)方法,它将在生成器内部最后一个yield时产生一个异常以及一个close方法,从而在生成器内部引发一个新的GeneratorExit异常来彻底终止迭代。

生成器表达式:当可迭代对象遇见推导语法

生成器表达式:从语法上来讲,生成器表达式就像一般的列表推导一样,而且也支持所有列表推导的语法,但它们是包括在圆括号而不是方括号中:

>>> [x ** 2 for x in range(5)]
[0, 1, 4, 9, 16]
>>> (x ** 2 for x in range(5))
<generator object <genexpr> at 0x108540450>

列表推导表达式的上下文中:我们拿到了一个在每次返回一个结果后还能记住它所生成位置的对象。同生成器函数一样,通过观察这些对象在它们自动支持的协议中运行,通过next函数调用:

>>> G = (x ** 2 for x in range(5))
>>> iter(G) is G
True
>>> 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
>>> G
<generator object <genexpr> at 0x1085405d0>cv

同样,我们一般不会在生成器表达式的底层看到next迭代器机制的使用,因为for循环会自动触发:

>>> for num in (x ** 2 for x in range(4)):
...     print(num)
... 
0
1
4
9

为什么要使用生成器表达式

就想生成器函数,生成器表达式是一种对内存空间的优化:它们不需要像方括号的列表推导一样,一次构造出整个结果列表。与生成器函数一样,它们将生成结果的过程拆分成更小的时间片:它们会一部分一部分地产生结果,而不是让调用者在一次调用中等持整个集合被创建出来。
另一方面,生成器表达式在实际中运行起来可能比列表推导稍慢一些,所以它们可能只对那些结果集合非常大的运算或者不能等待全部数据产生的应用来说是最优选择。关于性能的更权威的评价,必须等到我们在下一章编写计时脚本的时候给出。

生成器函数 vs 生成器表达式

  • 生成器函数
    一个使用了 yield表达式的def语句是一个生成器函数。当被调用时,它返回一个新的生成器对象,该对象能自动保存局部作用域和代码执行位置,一个自动创建的ter方法能够返回自身,一个自动创建的_next_方法(在2x中为next)用于启动函数或从上次出的地方继续执行,井在结果产生结束的时候引发 StopIteration异常。
  • 生成器表达式
    一个包括在圆括号中的列表推导表达式被称为一个生成器表达式。当它运行时,会返回一个新的生成器对象,这个对象带有同样是被自动创建的方法接口和状态保持,一同作为生成器函数调用的结果一包括一个返回自身__iter__方法和一个启动隐式循环或从上次运行离开的地方重新开始的__next__方法,并且在结束产生结果的时候引发 StopIteration异常。

生成器是单遍迭代对象

一个十分细微但是重要的注意点:生成器函数和生成器表达式自身都是迭代器,并因此只支持一次活跃迭代——不像一些内置类型,我们无法拥有在结构集中位于不同位置的多个迭代器。

>>> G = (c ** 4 for c in 'spam')
>>> iter(G) is G
True

如果尝试手动地使用多个迭代器来迭代结果流,它们将指向相同的位置:

>>> G = (c * 4 for c in 'spam')
>>> I1 = iter(G)
>>> 
>>> next(I1)
'ssss'
>>> next(I1)
'pppp'
>>> 
>>> I2 = iter(G)
>>> 
>>> next(I2)
'aaaa'

一旦任一迭代器运行结束,所有的迭代器都将用尽——我们必须产生一个新的生成器一遍重新开始:

>>> next(I2)
'mmmm'
>>> 
>>> next(I1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>
>>>
>>>
>>> I3 = iter(G)
>>> 
>>> next(I3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> 
>>> I3 = (c * 4 for c in 'spam')
>>> next(I3)
'ssss'
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值