如何理解 Python 中的可迭代对象、迭代器和生成器
▍前言
在讨论可迭代对象、迭代器和生成器之前,先说明一下迭代器模式(iterator pattern),维基百科这么解释:
迭代器是一种最简单也最常见的设计模式。它可以让用户透过特定的接口巡访容器中的每一个元素而不用了解底层的实现。
迭代是数据处理的基石。当内存中放不下数据集时,我们要找到一种惰性获取数据的方式,即按需一次获取一个数据项,这就是迭代器模式。
▍序列可迭代的原因:iter函数
我们都知道序列是可迭代的。当解释器需要迭代对象x时,会自动调用iter(x)。
内置的iter函数有以下作用:
- 检查对象是否实现了__iter__方法,如果实现了就调用它,获得一个迭代器。
- 如果没有实现__iter__方法,但是实现了__getitem__方法,python会创建一个迭代器,尝试按顺序(从索引0开始)获取元素。
- 如果尝试失败,python会抛出`TypeError`异常,通常会提示"C object is not iterable",其中C是目标对象所属的类。
截止到Python3.6,基本上所有的Python序列也都实现了__getitem__方法,这是保证任何序列都可迭代的原因。当然标准的序列也都实现了__iter__方法,之所以对__getitem__也可以创建迭代器是为了向后兼容,未来可能不在这么做。
但是,从Python3.4开始,检查x能否迭代,最准确的方法是调用iter(x)函数,如果不可迭代,再处理TypeError异常。
▍可迭代对象与迭代器关系
使用iter内置函数可以获取迭代器对象。也就是说,如果一个对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的,序列都可以迭代;实现了__getitem__方法,而且其参数是从零开始的索引,这种对象也是可迭代的。
因此可以明确可迭代对象和迭代器之间的关系:Python从可迭代的对象中获取迭代器。
标准的迭代器接口有两个方法,即:
- __next__:返回下一个可用元素,如果没有元素,抛出StopIteration异常
- __iter__:返回self,以便在应该使用可迭代对象的地方使用迭代器,比如for循环中。
因为迭代器只需__next__和__iter__两个方法,所以除了调用next()方法,以及捕获StopIteration异常之外,没有办法检查是否还有遗留的元素。此外,也没有办法还原迭代器。如果想再次迭代,那就要调用iter(…),传入之前构建迭代器的可迭代对象。
构建可迭代对象和迭代器时经常会出现错误,原因是混淆了两者。要知道,可迭代的对象有个__iter__方法,调用该方法每次都实例化一个新的迭代器;而迭代器要实现__next__方法,返回单个元素,此外还要实现__iter__方法,返回迭代器本身(self),如图。因此,迭代器可以迭代,但是可迭代的对象不是迭代器。
需要注意的是:
可迭代的对象必须实现__iter__方法,但不能实现__next__方法。另一方面,迭代器应该一直可以迭代,迭代器的__iter__方法应该返回自身。虽然可迭代对象和迭代器都有__iter__方法,但是两者的功能不一样,再次强调一下,可迭代对象的__iter__用于实例化一个迭代器对象,而迭代器中的__iter__用于返回迭代器本身,与__next__共同完成迭代器的迭代作用。
▍生成器
在Python中创建迭代器最方便的方法是使用生成器。生成器也是迭代器。生成器的语法类似于函数,但是不返回值。为了显示序列中的每一个元素,会使用yield语句。只要Python函数的定义体中有yield关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器对象。
获取生成器通常有两种方式,生成器函数和生成器表达式。
生成器函数
def gen_123(): # 只要Python代码中包含yield,该函数就是生成器函数
yield 1 #生成器函数的定义体中通常都有循环,不过这不是必要条件;此处重复使用了3次yield
yield 2
yield 3
if __name__ == '__main__':
print(gen_123) # 可以看出gen_123是函数对象
# <function gen_123 at 0x10be19>
print(gen_123()) # 函数调用时返回的是一个生成器对象
# <generator object gen_123 at 0x10be31>
for i in gen_123(): # 生成器是迭代器,会生成传给yield关键字的表达式的值
print(i)
g = gen_123() # 为了仔细检查,把生成器对象赋值给g
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
print(next(g)) # 生成器函数的定义体执行完毕后,生成器对象会抛出异常。
# Traceback (most recent call last):
# File "test.py", line 17, in <module>
# print(next(g))
# StopIteration
- 只要Python代码中包含yield,该函数就是生成器函数
- 生成器函数的定义体中通常都有循环,不过这不是必要条件;此处重复使用了3次yield
- 可以看出gen_123是函数对象
- 函数调用时返回的是一个生成器对象
- 生成器是迭代器,会生成传给yield关键字的表达式的值
- 为了仔细检查,把生成器对象赋值给g
- 因为g是迭代器,所以调用next(g)会获取yield生成的下一个元素
- 生成器函数的定义体执行完毕后,生成器对象会抛出异常。
生成器表达式
简单的生成器函数,可以替换成生成器表达式。生成器表达式可以理解为列表推导的惰性版本:不会迫切的构建列表,而是返回一个生成器,按需惰性生成元素。也就是说,如果列表推导是制造工厂的列表,那么生成器表达式就是制造生成器的工厂。如下演示了一个简单的生成器表达式,并且与列表推导做了对比。
In [1]: def gen_AB(): # 1
...: print('start')
...: yield 'A'
...: print('continue')
...: yield 'B'
...: print('end.')
...:
In [2]: res1 = [x*3 for x in gen_AB()] # 2
start
continue
end.
In [3]: for i in res1(): # 3
...: print('-->', i)
...:
AAA
BBB
In [4]: res2 = (x*3 for x in gen_AB()) # 4
In [5]: res2 # 5
<generator object <genexpr> at 0x106a07620>
In [6]: for i in res2(): # 6
...: print('-->', i)
...:
start
--> A
continue
--> B
end.
- #1-创建gen_AB函数
- #2-列表推到迫切的迭代gen_AB()函数生成的生成器对象产出的元素:’A’和’B’。注意。下面输出的是start、continue、end.。
- #3-for循环迭代列表推导生成的res1列表
- #4-把生成器表达式返回的值赋值给res2。只需调用gen_AB()函数,虽然调用时会返回一个生成器,但是这里并不使用。
- #5-可以看出res2是一个生成器对象。
- #6-只有for循环迭代res2时,gen_AB函数的定义体才会真正执行。for循环每次迭代时会隐式调用next(res2),前进到gen_AB函数中的下一个yield语句。注意,gen_AB函数的输出与for循环中print函数的输出夹杂在一起。
生成器表达式是创建生成器的简洁句法,这样无需定义函数再调用。不过,生成器函数灵活的多,可以使用多个语句实现复杂的逻辑,也可以作为协程(后面有机会讲到)使用。遇到简单的情况时,可以使用生成器表达式,因为这样扫一眼就知道代码的作用。其实选择那种句法很容易判断:如果生成器表达式需要分行写,倾向于定义成生成器函数,以便提高可读性;此外生成器函数有名称,因此可以重用。
a = (i for i in range(10))
'__next__' in dir(a) # True
'__iter__' in dir(a) # True
以上可以看出,生成器也是一种迭代器!
前面我们提到了惰性计算。其实,我们有时候使用生成器而不是传统的函数时,正是因为惰性计算有好处---只计算需要的数据,并且整个系列不需要一次性全部驻留在内存中。事实上,生成器完全可以有效的生产数值的无限序列,举一个例子,斐波那契数列是一个经典的无限数字序列,下面用生成器可以产生这个无穷级数:
def fibonacci():
a = 0
b = 1
while True:
yield a
future = a + b
a = b
b = future
以上!
版权声明: 本文为 InfoQ 作者【王坤祥】的原创文章。