python中迭代器与生成器

参考文章:https://foofish.net/iterators-vs-generators.html

一直对python迭代器和生成器的概念不理解,其实没有什么特别复杂的概念,只是自己没有静下心来好好理解。


概念

提到迭代器和生成器,就会牵扯到另外两个概念:可迭代对象和容器。所以我们要整体介绍一下容器、可迭代对象、迭代器、生成器,而且这样的排序从概念上来说是依次深入具体的。先从一个图片开始。
迭代器生成器关系总览

先整体说一下这几种概念之间的关系:

  1. 容器是一种数据结构,能够存放不同类型的数据到容器内部,常见的容器对象比如list,set,dict,truple,str等。
  2. 可迭代对象是一种能够返回迭代器的对象。
  3. 迭代器就是可以循环迭代访问可迭代对象内部数据的一种工厂模式的对象。
  4. 生成器从字面意思上看生成器是生成可迭代对象或者迭代器的一种函数或表达式的统称吗?不是的,生成器是一种特殊的迭代器,本质上来说也是迭代器。

容器

容器是一种能够存放不同类型数据的数据结构的统称,容器中的元素可以逐个迭代获取,也可以使用is,not in关键字判断元素是否在容器中。可以将容器理解为一个盒子、房子或者箱子,里面可以放你想要的东西,当对一个对象进行元素获取或者判断元素是否在其内部时,这个对象就可以认为是一个容器,比如list、set、truples都是容器对象。

1
2
3
4
5
6
>>> assert 1 in [1, 2, 3]      # lists
>>> assert 4 not in [1, 2, 3]
>>> assert 1 in {1, 2, 3} # sets
>>> assert 4 not in {1, 2, 3}
>>> assert 1 in (1, 2, 3) # tuples
>>> assert 4 not in (1, 2, 3)

我们能够访问容器中的元素,不是因为容器本身具备被迭代访问的功能,而是因为容器一般都是可迭代对象的原因,这里就引出了可迭代对象的概念。

可迭代对象

我们说容器因为是可迭代对象,所以容器内的元素可以被迭代访问(就是循环获取容器里的元素数据),那么可迭代对象是什么,为什么是可迭代对象的话就可以获取容器内部的元素数据?

任何能够返回迭代器的对象我们就称之为可迭代对象,常见的我们创建的list对象,set对象,dict对象都是可迭代对象,它的类型分别是list_iterator、set_iterator、dict_iterator,因为这些内置类型在设计之初就被设计成了可迭代对象,强制设定,你必须了解。这里我们引出了迭代器的概念。不着急,我们稍后再说迭代器。

我们刚才说能够返回迭代器的对象我们称之为可迭代对象,具体来说就是可迭代对象内部实现了iter方法,该方法返回了一个迭代器对象。
运行代码:

1
2
3
x = [1, 2, 3]
for elem in x:
...

实际情况是:
这样的

我们创建了一个list对象x,x是可迭代对象,使用for循环可以进行迭代访问,这里for循环其实做了两个事,第一是获取可迭代对象x的迭代器,这其中是使用了可迭代对象x的iter()方法;第二是调用迭代器的next()方法,获取x内部的值。

听到这里可能还是有点懵,肯定想知道迭代器是什么,马上开始介绍。

迭代器

迭代器是带状态的对象,我们通过for循环能够获取容器内部所有元素的值就是因为迭代器是有状态的,能够记录你已经获取操作执行到了哪里。我们说可迭代对象是任何能够返回迭代器的对象就是可迭代对象,而可迭代对象的实现是通过iter方法返回一个迭代器对象实现的。迭代器是任何实现了iternext方法的对象都是迭代器,iter返回迭代器自身,next返回容器中的下一个值,如果容器中没有元素了,就抛出异常。

为了直观感受迭代器内部执行过程,我们自定义一个迭代器,以斐波那契数列为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Fib:
def __init__(self):
self.prev = 0
self.curr = 1

def __iter__(self):
return self

def __next__(self):
value = self.curr
self.curr += self.prev
self.prev = value
return value

>>> f = Fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Fib既是一个可迭代对象(因为它实现了iter方法),又是一个迭代器(因为它实现了next方法),实例变量curr和prev用于维护迭代器内部状态,每次调用next()方法时迭代器做两件事情:

  1. 为下一次调用next()方法修改状态。

  2. 为当前这次调用返回结果。

迭代器在有人使用的时候才返回值,没有调用的时候就处于休眠状态等待下一次调用。

生成器

生成器是一种特殊的迭代器,是一种更优雅的迭代器,在书写时候不需要像上面迭代器一样实现iternext方法,只需要一个yield关键字。生成器一定是迭代器,反之不成立,因此任何生成器也都是以一种懒加载的模式生成值。用生成器来实现斐波那契数列的例子是:

1
2
3
4
5
6
7
8
9
def fib():
prev, curr = 0, 1
while True:
yield curr
prev, curr = curr, curr + prev

>>> f = fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

fib是一个普通的python函数,函数没有return关键字,函数的返回值是一个生成器对象,当执行f = fib()时返回一个生成器对象,此时函数中的代码并不执行,只有显式或隐式调用next()函数时才执行里面的代码。

生成器在Python中是一个非常强大的编程结构,可以用更少地中间变量写流式代码,此外,相比其它容器对象它更能节省内存和CPU,当然它可以用更少的代码来实现相似的功能。

生成器表达式

生成器表达式是列表推倒式的生成器版本,看起来像列表推导式,但是它返回的是一个生成器对象而不是列表对象。

1
2
3
4
5
>>> a = (x*x for x in range(10))
>>> a
<generator object <genexpr> at 0x401f08>
>>> sum(a)
285

总结

  • 容器是一系列元素的集合,str、list、set、dict、file、sockets对象都可以看作是容器,容器都可以被迭代(用在for,while等语句中),因此他们被称为可迭代对象。
  • 可迭代对象实现了iter方法,该方法返回一个迭代器对象。
  • 迭代器持有一个内部状态的字段,用于记录下次迭代返回值,它实现了nextiter方法,迭代器不会一次性把所有元素加载到内存,而是需要的时候才生成返回结果。
  • 生成器是一种特殊的迭代器,它的返回值不是通过return而是用yield。