Python 生成器

Python 专栏收录该内容
29 篇文章 0 订阅

概述

生成器是一个相对较新的Python概念,它是一种使用普通函数语法定义的迭代器。生成器和迭代器可能是今年来引入的最强大的功能,并且生成器是一个相当复杂的概念,要了解其工作原理需要花点时间。生成器能够让你编写出非常优雅的代码。

创建生成器

生成器创建和普通函数一样简单,下面通过一个简单的示例来说明生成器的用法:

创建一个将嵌套列表展开的函数,列表如下:

nested = [[1,2,3],[3],[4,5]]

这是一个列表的列表,接下来需要将嵌套展开或按顺序提供这些数字,下面是一种生成器的解决方案:

def flatten(nested):
	for sublist in nested:
		for element in sublist:
			yield(element)

这个函数代码很简单,它首先迭代所提供嵌套列表中的所有子列表,然后按顺序迭代每个子列表中的元素,如果将最后一行中的yield换成print就容易理解了。

注意,这里使用了关键字yield,包含yield语句的函数都被称为生成器

这不仅仅是名称上的差别,生成器的行为与普通函数截然不同。差别在于,生成器不是使用return返回一个值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤起。被重新唤起后,函数将从停止的地方开始继续执行。

为使用所有的值,可对生成器进行迭代。

print(list(flatten(nested)))

结果:

[1, 2, 3, 3, 4, 5]

递归式生成器

上面的示例智能处理两层的嵌套逻辑,使用两个for循环来实现的,如果要处理任意层嵌套的列表,这时候就不能用for循环来实现了,所以必须修改解决方案,可以使用递归式生成器。

使用递归式生成器重写以上函数:

def flatten(nested):
	try:
		for sublist in nested:
			for element in flatten(sublist):
				yield(element)
	except TypeError:
		yield nested

调用flatten时,有两种可能性:基线条件和递归条件。在基线条件下,要求这个函数展开单个元素。这时候,for循环将引发TypeError异常,因为你视图迭代一个数,而这个生辰器只生成一个元素。

如果要展开的是一个列表(或其他任何可迭代对象),你就需要遍历所有子列表并对他们调用flatten,然后使用另一个for循环生成展开后的子列表中的所有元素。

print(list(flatten([[1,2,3],[1],[4,[5,6],7]])))

输出:

[1, 2, 3, 1, 4, 5, 6, 7]

但是,以上这个示例也存在一个问题,如果nested是字符串或类似字符串的对象,它就属于序列,因此不会引发TypeError异常,可你并不想对其进行迭代。

注意:在以上函数中,不应该对类似于字符串的对象进行迭代,主要原因有两个:

  • 1.你想将类似于字符串的对象视为原子值,而不是应该展开的序列
  • 2.对这样的对象进行迭代会导致无穷递归,因为字符串的第一个元素是一个长度为1的字符串,而长度为1的字符串的第一个元素为字符串本身。

所以,要处理这种情况,必须在生成器开头进行检查,要检查对象是否类似于字符串,最简单、最快捷的方式是将对象与一个字符串拼接起来,并检查这是否会引发TypeError异常,修改如下:

def flatten(nested):
	try:
		try:
			nested + ''
		except TypeError:
			pass
		else:
			raise TypeError
			
		for sublist in nested:
			for element in flatten(sublist):
				yield(element)
	except TypeError:
		yield nested
print(list(flatten(['abc',[1,2],['ed','fg']])))

输出:

['abc', 1, 2, 'ed', 'fg']

如你所见,如果表达式nested+’'引发TypeError异常,就忽略这种异常;如果没有引发TypeError异常,内部try语句中的else子句将引发TypeError异常,这样讲在外部的excpet子句中原封不动地生成类似于字符串的对象。

通用生成器

生成器由两个单独的部分组成:生成器的函数生成器的迭代器。生成器的函数是由def语句定义,其中包含yield。生成器的迭代器是这个函数的返回结果,这两个实体通常被视为一个,通称为生成器

>>> def simple_generator():
...     yield 1
... 
>>> simple_generator
<function simple_generator at 0x104e4ab70>
>>> simple_generator()
<generator object simple_generator at 0x104d65a20>
>>> 

对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。

生成器的方法

生成器开始运行后,可以使用生成器和外部之间的通信渠道向它提供值,这个通信渠道包含如下两个端点:

  • 外部世界:外部世界可访问生成器的方法send,这个方法类似于next,但接受一个参数(要发送的“消息”,可以是任何对象)
  • 生成器:在挂起的生成器内部,yield可能用作表达式而不是语句,也就是说,当生成器重新运行时,yield返回一个值----通过send从外部世界发送的值,如果使用的是next,yield将返回None。

注意,仅当生成器被挂起(即遇到第一个yield)后,使用send(而不是next)才有意义。要在此之前向生成器提供信息,可使用生成器的函数的参数。
如果一定要在生成器刚启动时对其调用方法send,可向它传递参数None。

def repeater(value):
	while True:
		new = (yield value)
		if new is not None:value = new

r = repeater(12)
print(next(r))
print(r.send(34))

输出

12
34

生成器的另外两个方法:

  • 方法throw:用于在生成器中(yield表达式处)引发异常,调用时可提供一个异常类型、一个可选值和一个traceback对象
  • 方法close:用于停止生成器,调用时无需提供任何参数。

方法close(由Python垃圾收集器在需要时调用)也是基于异常的:在yield处引发GeneratorExit异常。因此如果要在生成器中提供一些清理代码,可将yield放在一条try/finally语句中。如果愿意,也可以捕获GeneratorExit异常,但随后必须重新引发它(可能在清理后)、引发其他异常或直接返回。对生成器调用close后,再试图从它那里获取值将导致RuntimeError异常。

模拟生成器

前面我们说到,生成器可以让你能够写出非常优雅的代码,但是,无论编写什么程序,都完全可以不使用生成器。并且在一些老版本的Python没有生成器的概念,那么,要模拟这种用法该怎么办呢。

下面提供了一个简单的解决方案,让你能够使用普通函数模拟生成器。

首先在函数的开头插入如下一行代码:
result=[]
然后,将类似于yield some_expression的代码行替换如下代码行:

result.append(some_expression)

最后,在函数末尾添加:return result

尽管这种方法并不能模拟所有的生成器,但可模拟大部分生成器。例如,这无法模拟无穷生成器,因为显然不能将这种生成器的所有值都存储在一个列表中。

下面使用普通函数重写了生成器flatten:

def flatten(nested):
	result = []
	try:
		try:
			nested + ''
		except TypeError:
			pass
		else:
			raise TypeError

		for sublist in nested:
			for element in flatten(sublist):
				# yield(element)
				result.append(element)
	except TypeError:
		# yield nested
		result.append(nested)
	return result
print(list(flatten([[1,2,3],[1],[4,[5,6],7]])))
print(list(flatten(['abc',[1,2],['ed','fg']])))

输出:

[1, 2, 3, 1, 4, 5, 6, 7]
['abc', 1, 2, 'ed', 'fg']
展开阅读全文
  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值