目录
1. 迭代器
1.1 迭代器协议
1.2 从迭代器创建序列
2. 生成器
2.2 递归式生成器
2.3 通用生成器
2.4 生成器的方法
2.5 模拟生成器*
在了解迭代器之前我们要知道上面是魔法方法,python中有些名称很特别,开头和结尾都是两个下划线(如__future__、__init__)。这样拼写表示名称有特殊意义,因此绝不要在程序中创建爱你这样的名称。在这样的名称中,很大一部分都是魔法(特殊)方法的名称。如果你的对象实现了这些方法,它们将在特定情况下(具体是那种情况取决于方法的名称)被python调用,而几乎不需要直接调用。对于魔法方法,迭代器只介绍__iter__,它是迭代协议的基础。
迭代(iterate)意味着重复多次,就像循环那样。我们可以使用for循环迭代序列和字典,但实际上也可以迭代其他对象:实现了方法__iter__的对象。
方法__iter__返回一个迭代器,它是包含__next__的对象,而调用这个方法时可不提供任何参数。当你调用方法__next__时,迭代器应返回其下一个值。如果跌迭代器没有可供返回的值,应引发StopIteration异常。你还可使用内置的便利函数next,在这种情况下,next(it)与 it.__next__等效。
迭代器的意义何在,列表不好吗?这是因为,使用列表有点像大炮打蚊子。例如,如果你有一个可逐个计算值的函数,你可能只想逐个地获取值,而不是使用列表一次性获取。这是因为如果有很多值,列表很可能占用太多的内存。还有其他原因:使用迭代器更通用、更简单、更加优雅。下面看一个不适用列表的例子,如果使用,这个列表的长度则需要是无穷大的。
此“列表”为斐波那契数列,表示该数列的迭代器如下:
class Fibs:
def __init__(self):
self.a = 0
self.b = 1
def __next__(self):
self.a, self.b = self.b, self.a + self.b
return self.a
def __iter__(self):
return self
注意到这个迭代器实现了方法__iter__,而这个方法返回迭代器本身。在很多情况下,都在另一个对象中实现返回迭代器的方法__iter__,并在for循环中使用这个对象。但推荐在迭代器中也实现方法__iter__(并像刚才那样让它返回self),这样迭代器就可以直接用于for循环中。
【注意】:正规定义是,实现了方法__iter__的对象是可迭代的,而实现了方法__next__的对象是迭代器。
首先创建一个Fibs对象。然后就可以在for循环中使用这个对象,如找出第一个大于1000的斐波那契数。
>>> fibs = Fibs()
>>> for i in fibs:
if i > 1000:
print(i)
break
通过对可迭代对象调用内置函数iter,可获得一个迭代器。
>>> it = iter([1,2,3])
>>> next(it)
1
>>> next(it)
2
还可以使用它从函数或其它可调用对象创建可迭代对象。
除了对迭代器和可迭代对象进行迭代(通常这样做)之外,还可以将它们转换为序列。在可以使用序列的情况下,大多也可以使用迭代器或可迭代对象(诸如索引和切片等操作除外)。一个这样的例子是使用构造函数list显示地将迭代器转换为列表。
class TestIterator:
value = 0
def __next__(self):
self.value += 1
if self.value > 10:
raise StopIterator
return self.value
def __iter__(self):
return self
>>> ti = TestIterator()
>>> list(ti)
[1,2,3,4,5,6,7,8,9,10]
生成器是一个相对较新的Python概念,它也被称为简单生成器(simple generator)。生成器和迭代器可能是近年来引入的最强大的功能,但生成器是一个相当复杂的概念,你可能需要花些功夫才能明白其工作原理和用途。虽然生成器让你能够编写出非常优雅的代码,但请放心,无论编写什么程序,都完全可以不使用生成器。生成器是一种使用普通函数语法定义的迭代器。生成器的工作原理到底是什么呢?通过示例来说明最合适。下面先来看看如何创建和使用生成器:
def flatten(nested):
for sublist in nested:
for element in sublist:
return element
在这里,包含yield的语句的函数被称为生成器。yield语句相比于retrun语句,差别就在于,yiled返回的是可迭代对象,而return返回的为不可迭代对象。这不仅仅是名称的区别,生成器的行为与普通函数也截然不同。差别就在于,生成器不是使用return返回一个值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停止执行。等待被重新唤醒。被重新唤醒后,函数从停止的地方开始继续执行。
为使用所有值,可对生成器进行迭代。
>>> nested = [[1, 2], [3, 4], [5]]
>>> for num in flatten(nested):
... print(num)
...
1
2
3
4
5
>>> list(flatten(nested))
[1, 2, 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循环生成展开后的子列表中的所有元素。这可能看起来不可思议,但确实可行。
>>> list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8]))
[1, 2, 3, 4, 5, 6, 7, 8]
然而,这个解决方案存在一个问题。如果nested是字符串或类似于字符串的对象,它就属于序列,因此不会引发TypeError异常,因为字符串永远有第一个元素,即使它只有一个字符,它的第一个元素就是它自己,这必然陷入无限循环(可以动手试试看,代码如下)!而实际上你也并不想对其进行迭代。
def flatten(nested):
try:
for sublist in nested:
for element in flatten(sublist):
yield element
except TypeError:
yield nested
a = list(flatten('happy'))
print(a)
要解决这一问题,必须在生成器开头进行检查。要检查对象是否类似于字符串,最简单的方式就是,尝试将对象与一个字符串拼接起来,并检查是否引发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
如你所见,如果表达式nested + ''引发了TypeError异常,就忽略这种异常;如果没有引发TypeError异常,内部try语句中的else子句将引发TypeError异常,这样将在外部的excpet子句中原封不动地生成类似于字符串的对象。这时候就有:
>>> list(flatten(['foo', ['bar', ['baz']]]))
['foo', 'bar', 'baz']
请注意,这里没有执行类型检查:我没有检查nested是否是字符串,而只是检查其行为是否类似于字符串,即能否与字符串拼接。对于这种检查,一种更自然的替代方案是,使用isinstance以及字符串和类似于字符串的对象的一些抽象超类,但遗憾的是没有这样的标准类。另外,即便是对UserString来说,也无法检查其类型是否为str。
生成器是包含关键字yield的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。每次请求值时,都将执行生成器的代码,直到遇到yield或return。yield意味着应生成一个值,而return意味着生成器应停止执行(即不再生成值;仅当在生成器调用return时,才能不提供任何参数)。
换而言之,生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数是由def语句定义的,其中包含yield。生成器的迭代器是这个函数返回的结果。用不太准确的话说,这两个实体通常被视为一个,通称为生成器。
>>> def simple_generator():
yield 1
...
>>> simple_generator
>>> simple_generator()
对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。
在生成器开始运行后,可使用生成器和外部之间的通信渠道向它提供值。这个通信渠道包含如下两个端点。
外部世界:外部世界可以访问生成器的方法send,这个方法类似于next,但接受一个参数(要发送的“消息”,可以使任何对象)
生成器:在挂起的生成器内部,yield可能用作表达式而不是语句。换言之,当生成器重新运行时,yield返回一个值——通过send从外部世界发送的值。如果使用的是next,yield将返回None。
请注意,仅当生成器被挂起(即遇到第一个yield)后,使用send(而不是next)才有意义。要在此之前向生成器提供信息,可使用生成器的函数的参数。下面是一个说明机制的示例:
def repeater(value):
while True:
new = (yield value)
if new is not None: value = new
>>> r = repeater(42)
>>> next(r)
42
>>> r.send("Hello, world!")
"Hello, world!"
注意到使用圆括号将yield表达式括起来了。在有些情况下,并非必须这样做,但小心驶得万年船。如果要以某种方式使用返回值,就不管三七二十一,将其用圆括号括起吧。
生成器还包含另外两个方法。
方法throw:用于在生成器中(yield表达式处)引发异常,调用时可提供一个异常类型、一个可选值和一个traceback对象。
方法close:用于停止生成器,调用时无需提供任何参数。
方法close(由Python垃圾收集器在需要时调用)也是基于异常的:在yield处引发GeneratorExit异常。因此如果要在生成器中提供一些清理代码,可将yield放在一条try/finally语句中。如果愿意,也可捕获GeneratorExit异常,但随后必须重新引发它(可能在清理后)、引发其他异常或直接返回。对生成器调用close后,再试图从它那里获取值将导致RuntimeError异常。
如果你使用的是较老的Python版本,就无法使用生成器。下面是一个简单的解决方案,让你能够使用普通函数模拟生成器。
首先,在函数体开头插入如下一行代码:
result = []
如果代码已使用名称result,应改用其他名称。(在任何情况下,使用更具描述性的名称都是不错的主意。)接下来,将类似于yield some_expression的代码行替换为如下代码行:
yield some_expression with this:
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):
result.append(element)
except TypeError:
result.append(nested)
return result