前言
在学习Python语言,不可避免遇到各种绕口的概念,比如可迭代对象、迭代器和生成器,新手看了第一印象就是一脸懵。为此,本文详细介绍和解释这些概念,让新手一看就会。
回顾Python中的数据存储结构,Python是用容器来存储数据,比如:list、dict、tuple、strset…等等,当数据量不大的时候用这些存储没什么问题,当数据量特别大甚至是无限的时候,就不能使用这些容器来存储了,于是Python有一种机制专门解决这种问题。
一、可迭代对象(iterable)和迭代器(iterator)
1.1 概念介绍
通俗点讲,可迭代对象就是可以产生迭代器的对象,所谓迭代器就像一个懒加载的工厂,本身不存储数据,只有你向它索要的时候临时给你生成数据。下面用一个例子来解释:
>>> x = [1, 2, 3]
>>> y = iter(x)
>>> z = iter(x)
>>> next(y)
1
>>> next(y)
2
>>> next(z)
1
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>
在这个例子中,x是一个list对象,是一个可迭代对象,通过iter()函数生成x的迭代器y,那么只有在调用next()方法时y才输出数据。了解了两者的基本内容,下面来深入讲解。
先抛出可迭代对象和迭代器的结论:
- 可迭代对象包含迭代器; 如果一个对象拥有__iter__方法,其是可迭代对象;
- 如果一个对象拥有next方法,其是迭代器;
- 定义可迭代对象,必须实现__iter__方法;定义迭代器,必须实现__iter__和__next__方法。
这三个结论看起来很绕,来解释一下,可迭代对象是一个对象,对象包含__iter__方法,我们可以使用iter()函数来生成该对象的迭代器,生成的迭代器包含__iter__和__next__方法。迭代器也有__iter__,说明迭代器也是可迭代对象,当对迭代器使用iter()函数时,返回的是该迭代器本身,而next方法不断地获取迭代器中的下一个元素,如果迭代器中没有更多元素了,则抛出StopIteration异常。
1.2 为什么要用迭代器
我们通过可迭代对象生成迭代器,那我们要用迭代器的目的是什么呢?开头讲了,迭代器的使用是为了节省空间,想象一下,通过列表生成式,我们可以直接创建一个列表,但是,受到内存限制,列表容量肯定是有限的,而且创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。通过例子加深印象:
生成无限序列:
>>> from itertools import count
>>> counter = count(start=13)
>>> next(counter)
13
>>> next(counter)
14
从一个有限序列中生成无限序列:
>>> from itertools import cycle
>>> colors = cycle(['red', 'white', 'blue'])
>>> next(colors)
'red'
>>> next(colors)
'white'
>>> next(colors)
'blue'
>>> next(colors)
'red'
从无限的序列中生成有限序列:
>>> from itertools import islice
>>> colors = cycle(['red', 'white', 'blue']) # infinite
>>> limited = islice(colors, 0, 4) # finite
>>> for x in limited:
... print(x)
red
white
blue
red
1.3 定义一个迭代器
为了进一步加深迭代器概念理解,我们建立一个生成斐波那契数列的迭代器:
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()
>>> print(next(f))
1
>>> print(next(f))
1
>>> print(next(f))
2
>>> print(next(f))
3
Fib既是一个可迭代对象(因为它实现了__iter__方法),又是一个迭代器(因为实现了__iter__和__next__方法)。实例变量prev和curr用户维护迭代器内部的状态。每次调用next()方法的时候做两件事:
- 为当前这次调用生成返回结果
- 为下一次调用next()方法修改状态
迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。
二、生成器(generator)
生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要再像上面的类一样写__iter__()和__next__()方法了,只需要一个yiled关键字。 生成器一定是迭代器(反之不成立),因此任何生成器也是以一种懒加载的模式生成值。
2.1 用生成器来实现斐波那契数列:
def fib():
print("Start...")
prev, curr = 0, 1
while True:
yield curr
prev, curr = curr, curr + prev
>>> f = fib()
>>> print(type(f))
<class 'generator'>
>>> print(next(f))
Start...
1
>>> print(next(f))
1
>>> print(next(f))
2
>>> print(next(f))
3
来解释一下流程:
- 程序开始执行以后,因为fib函数中有yield关键字,所以fib函数并不会真的执行,而是先得到一个生成器f(相当于一个对象);
- 直到第一次调用next方法,fib函数正式开始执行,先执行fib函数中的print和prev, curr = 0, 1,然后进入while循环,程序遇到yield关键字,然后把yield想想成return,return了一个curr之后,程序停止,并没有执行函数后面的代码;
- 又开始执行下面的print(next(f)),这个时候和上面那个差不多,不过不同的是,这个时候是从刚才那个next程序停止的地方开始执行的,也就是要执行prev, curr = curr, curr + prev,然后循环又遇到yield,return变量curr后程序又停止。
到这里你可能就明白yield了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从fib函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。
2.2 Send()方法
def fun():
print("starting...")
while True:
res = yield 4
print("res:",res)
>>> f = fun()
>>> print(next(f))
starting...
4
>>> print(f.send(23))
res:23
4
来解释一下流程:
- f = fun()因为fun函数里面有yield关键字,所以函数不会执行并且返回一个生成器;
- 第一次执行next()方法,开始执行fun函数,直到遇到yield,抛出yield后面的对象,程序停止;
- 程序执行f.send(23),程序会从上次停止的地方开始执行,因为send了一个23,所以上次停止的地方出现一个23,由于send方法中包含next()方法,所以程序会继续向下运行执行,res赋值为23,执行print,然后再次进入while循环,遇到yield,抛出yield后面的对象,程序停止;
三、总结
迭代器持有一个内部状态的字段,用于记录下次迭代返回值,它实现了__next__和__iter__方法,迭代器不会一次性把所有元素加载到内存,而是需要的时候才生成返回结果。
生成器是一种特殊的迭代器,它的返回值不是通过return而是用yield。
来源:CSDN
作者:某八爪鱼的锅
链接:https://blog.csdn.net/qq_29051413/article/details/103632407