Python迭代器和生成器详解
概述
在使用Python的过程中,经常会和列表/元组/字典(list/tuple/dict)、容器(container)、可迭代对象(iterable)、迭代器(iterator)、生成器(generator)等这些名词打交道,众多的概念掺杂到一起难免会让人一头雾水,这里我们用一张图来展现它们之间的关系。
今天我们主要讲解迭代器(iterator)和生成器(generator),在这之前我们先来看下容器(container)和可迭代对象(iterable)。
那么什么是容器呢?
容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个迭代获取,可以用 in,not in 关键字判断元素是否包含在容器中。通常这类数据结构把所有的元素存储在内存中(也有一些特例,并不是所有的元素都放在内存,比如迭代器和生成器对象),我们常用的 string、set、list、tuple、dict 都属于容器对象。
# string
>>> 'h' in 'hello'
>>> 'z' in 'hello'
False
>>> 'z' not in 'hello'
# list
>>> 1 in [1, 2, 3]
>>> 5 in [1, 2, 3]
False
>>> 5 not in [1, 2, 3]
# set
>>> 1 in {1, 2, 3}
>>> 5 in {1, 2, 3}
False
>>> 5 not in {1, 2, 3}
# tuple
>>> 1 in (1, 2, 3)
>>> 5 in (1, 2, 3)
False
>>> 5 not in (1, 2, 3)
# dict
>>> 1 in {1:'one', 2:'two', 3:'three'}
>>> 5 in {1:'one', 2:'two', 3:'three'}
False
>>> 5 not in {1:'one', 2:'two', 3:'three'}
True
尽管大多数容器都提供了某种方式获取其中的每一个元素,但这并不是容器本身提供的能力,而是可迭代对象赋予了容器这种能力,当然并不是所有容器都是可迭代的。
那什么是可迭代对象呢?
可以返回一个迭代器的对象都可以称之为可迭代对象,我们来看一个例子:
>>> x = [1, 2, 3]
>>> a = iter(x)
>>> b = iter(x)
>>> next(a)
>>> next(a)
>>> next(b)
>>> type(x)
<class 'list'>
>>> type(a)
<class 'list_iterator'>
>>> type(b)
<class 'list_iterator'>
这里的 x 是一个可迭代对象,可迭代对象和容器一样是一种通俗的叫法,并不是指某种具体的数据类型,list 是可迭代对象,dict 也是可迭代对象。a 和 b 是两个独立的迭代器,迭代器内部有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代时获取正确的元素。迭代器有一种具体的迭代器类型,比如
list_iterator
,
set_iterator
。可迭代对象实现了
__iter__()
方法,该方法返回一个迭代器对象。
当运行代码:
x = [1, 2, 3]
for ele in x:
...
实际执行情况是:
在循环遍历自定义容器对象时,会使用python内置函数iter()调用遍历对象的
__iter__()
获得一个迭代器,之后再循环对这个迭代器使用
next()
调用迭代器对象的
__next__()
。
__iter__()
只会被调用一次,而
__next__()
会被调用 n 次。
Iterator(迭代器)
迭代器是一个带状态的对象,它能在你调用
next()
方法时返回容器中的下一个值,任何实现了
__iter__()
和
__next__()
(Python2.x中实现
next()
)方法的对象都是迭代器,
__iter__()
返回迭代器自身,
__next__()
返回容器中的下一个值,如果容器中没有更多元素了,则抛出
StopIteration
异常。
迭代器与列表的区别在于,构建迭代器的时候,不像列表把所有元素一次性加载到内存,而是以一种延迟计算(lazy evaluation)方式返回元素,这正是它的优点。比如列表中含有一千万个整数,需要占超过100M的内存,而迭代器只需要几十个字节的空间。因为它并没有把所有元素装载到内存中,而是等到调用
next()
方法的时候才返回该元素(按需调用 call by need 的方式,本质上 for 循环就是不断地调用迭代器的
next()
方法)。
itertools模块里的函数返回的都是迭代器对象。为了更直观的感受迭代器内部的执行过程,我们自定义一个迭代器,以斐波那契数列为例:
#-*- coding:utf8 -*-
class Fib(object):
def __init__(self, max=0):
super(Fib, self).__init__()
self.prev = 0
self.curr = 1
self.max = max
def __iter__(self):
return self
def __next__(self):
if self.max > 0:
self.max -= 1
# 当前要返回的元素的值
value = self.curr
# 下一个要返回的元素的值
self.curr += self.prev
# 设置下一个元素的上一个元素的值
self.prev = value
return value
else:
raise StopIteration
# 兼容Python2.x
def next(self):
return self.__next__()
if __name__ == '__main__':
fib = Fib(10)
# 调用next()的过程
for n in fib:
print(n)
# raise StopIteration
print(next(fib))
斐波那契数列指的是 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ... 这样一个数列,这个数列从第三项开始,每一项都等于前两项之和。
Generator(生成器)
了解了迭代器之后,我们来看下生成器,普通函数用
return
返回一个值,还有一种函数用
yield
返回值,这种函数叫生成器函数。函数被调用时会返回一个生成器对象。生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅,它不需要像普通迭代器一样实现
__iter__()
和
__next__()
方法了,只需要一个
yield
关键字。生成器一定是迭代器(反之不成立),因此任何生成器也是一种懒加载的模式生成值。下面来用生成器来实现斐波那契数列的例子:
#-*- coding:utf8 -*-
def fib(max):
prev, curr = 0, 1
while max > 0:
max -= 1
yield curr
prev, curr = curr, prev + curr
if __name__ == '__main__':
fib = fib(6)
# 调用next()的过程
for n in fib:
print(n)
# raise StopIteration
print(next(fib))
上面是生成器函数,再来看下生成器的表达式,生成器表达式是列表推导式的生成器版本,看起来像列表推导式,但是它返回的是一个生成器对象而不是列表对象。
>>> x = (x*x for x in range(10))
>>> type(x)
<class 'generator'>
>>> y = [x*x for x in range(10)]
>>> type(y)
<class 'list'>
我们再看看一个例子:
#-*- coding:utf8 -*-
import time
def func(n):
for i in range(0, n):
# yield相当于return,下一次循环从yield的下一行开始
arg = yield i
print('func', arg)
if __name__ == '__main__':
f = func(6)
while True:
print('main-next:', next(f))
print('main-send:', f.send(100))
time.sleep(1)
运行结果为:
main-next: 0
func 100
main-send: 1
func None
main-next: 2
func 100
main-send: 3
func None
main-next: 4
func 100