【进阶Python】第五讲:迭代器与生成器
python 迭代器和生成器
迭代是Python中常用且非常强大的一个功能,它可以用于访问集合、列表、字符串、字典等数据结构的元素。
我们经常使用循环和条件语句,我们也清楚哪些是可以迭代访问,但是具体它们之间有什么有什么异同之处?有哪些特点?什么是迭代器、什么是生成器、什么是可迭代对象?
1 可迭代对象
可迭代对象是Python中一个非常庞大的概念,它主要包括如下三类:
迭代器
序列
字典
从上图可以看出不同概念之间的关系,迭代器是可迭代对象的一个子集,而生成器又是迭代器的一个子集,是一种特殊的迭代器。除了迭代器之外,Python中还有序列、字典等可迭代对象。
现在已经直观的了解了可迭代对象与迭代器、生成器之间的关系,那么用Python语言怎么表述它们的区别呢?
可迭代对象需要实现__iter__方法。
迭代器不仅要实现__iter__方法,还需要实现__next__方法。
在使用层面,可迭代对象可以通过in和not in访问对象中的元素。
X = set([1,2,3,4,5])
print(X)
print(type(X))
print(1 in X)
print(2 not in X)
for x in X:
print(x)
前面提到,可迭代对象实现了__iter__方法,但是它没有实现__next__,这也是判定迭代器和其他可迭代对象的关键之处,可以看一下通过next访问上述示例中可迭代对象X会报错。
next(X)
TypeError: 'set' object is not an iterator
它指明了set集合是一个可迭代对象,但不是迭代器。
2 迭代器
迭代器是可迭代对象的一个子集,它是一个可以记住遍历的位置的对象,它与列表、元组、集合、字符串这些可迭代对象的区别就在于next方法的实现,其他列表、元组、集合、字符串这些可迭代对象可以很简单的转化成迭代器。
迭代器是一个实现了迭代器协议的对象,Python中的迭代器协议就是有__next__方法的对象会前进到下一结果,而到一系列结果的末尾,则会引发StopIteration。任何这类的对象在Python中都可以用for循环或其他遍历工具迭代,迭代工具内部会在每次迭代时调用next方法,并且捕捉StopIteration异常来确定何时离开。
2.1 为什么要用迭代器
使用迭代器一个显而易见的好处就是:每次只从对象中读取一条数据,不会造成内存的过大开销。
比如要逐行读取一个文件的内容,利用readlines()方法,我们可以这么写:
from memory_profiler import profile
@profile
def test():
fr = open("use.txt",encoding="utf-8").readlines()
print(type(fr))
for line in fr:
print(line)
if __name__ == '__main__':
test()
这样虽然可以工作,但不是最好的方法。因为他实际上是把文件一次加载到内存中,然后逐行打印。当文件很大时,这个方法的内存开销就很大了。
利用file的迭代器,我们可以这样写:
from memory_profiler import profile
@profile
def test():
fr = open("use.txt",encoding="utf-8")
print(type(fr))
for line in fr:
print(line)
if __name__ == '__main__':
test()
这是最简单也是运行速度最快的写法,他并没显式的读取文件,而是利用迭代器每次读取下一行。
2.2 内置的iter函数
通过Python内置的iter函数能够轻松把可迭代对象转化为迭代器。
X = [1,2,3]
print(type(X))
Y = iter(X)
print(type(Y),Y)
print(next(Y))
print(next(Y))
print(next(Y))
print(next(Y))
从上述示例中我们可以看出两点:
(1)通过iter函数把list转化成了迭代器。
(2)可迭代器能够记住遍历位置,能够通过next方法不断从前往后访问。
2.3 自定义迭代器
可以自己通过实现__iter__和__next__方法来定义迭代器。
class Iterator(object):
def __init__(self, array):
self.x = array
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index < len(self.x):
value = self.x[self.index]
self.index += 1
else:
raise StopIteration
return value
it = Iterator([1,2,3,4,5])
print(type(it))
for i in it:
print(i)
3 生成器
从文章开头的流程图可以直观的看出,生成器是迭代器的子集,换句话说,生成器一定是迭代器,但是迭代器不全是生成器对象。
提及生成器就不得不提及一个Python中的关键字yiled,在Python中一个函数可以用yiled替代return返回值,这样的话这个函数就变成了一个生成器对象。
3.1 return返回方式
def generator(array):
for i in array:
return i
gen = generator([1, 2, 3])
print(type(gen),gen)
print(type(gen),gen)
输出
<class 'int'> 1
<class 'int'> 1
这是我们常见的return返回方式,这样的话generator函数获取的是一个int型对象。下面看一下换成yield关键字。
3.2 yield方式返回
def generator(array):
for i in array:
yield (i)
gen = generator([1, 2, 3])
print(type(gen),gen)
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))
这样的话获取的是一个生成器generator。
3.3 yield from方式返回
除了yield之外,在Python3.3之后还加入了yield from获取生成器,允许一个生成器将其部分操作委派给另一个生成器,使得生成器的用法变得更加简洁,yield from后面需要加上可迭代对象,这样可以把可迭代对象变成生成器,当然,这里的可迭代对象不仅包含列表、元组,还包含迭代器、生成器。yield from相对于yield的有几个主要优点:
(1)代码更加简洁。
(2)可以用于生成器嵌套。
(3)易于异常处理。
当我们需要访问多层/多维可迭代对象时,我们就不需要逐层的去用for … in …去访问,可以简单的通过yiled from把生成器委派给子生成器。
def generator(array):
for sub_array in array:
yield from sub_array
gen = generator([(1,2), (4,5)])
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))
输出
1
2
4
5
生成器可以像迭代器那样使用iter和next方法。
读到这里可以会有疑惑,从这个示例看来生成器和迭代器并没有什么区别啊?为什么生成器还可以称得上是Python中的一大亮点?
首先它对比于迭代器在编码方面更加简洁,这是显而易见的,其次生成器运行速度更快,最后一点,也是需要着重说明的一点:节省内存。
也许在一些理论性实验、学术论文阶段可以不考虑这些工程化的问题,但是在公司做项目时,内存和资源占用是无法逃避的问题 。如果我们使用其他可迭代对象处理庞大的数据时,当创建或者返回值时会申请用于存储整个可迭代对象的内存,显然这是非常浪费的,因为有的元素当前我们用不到,也不会去访问,但它却一直占用这内存。这时候就体现了生成器的优点,它不是一次性把所有的结果都返回,而是当我们每读取一次,它会返回一个结果,当我们不读取时,它就是一个生成器表达式,几乎不占用内存。
3.4 生成器表达式
X = [1, 2, 3, 4, 5]
it = [i for i in X]
gen = (i for i in X)
print(type(X))
print(type(it))
print(type(gen))
# 输出
<class 'list'>
<class 'list'>
<class 'generator'>
首先说一下it = [i for i in X],这种用法叫做列表生成式,在很多编程规范中非常推崇的一种替代for循环的方式,仔细看一下代码会发现,it = [i for i in X]与gen = (i for i in X)的区别非常小,只是一个用了中括号,一个用了小括号,但是它们的区别缺失非常大的,使用中括号的叫做列表生成式,获得的返回值是一个列表,而使用小括号叫做生成器表达式,获得的返回结果是一个生成器,这也是前面提到的,除了使用yield和yield from两个关键字外还可以使用生成器表达式获得生成器。
4 对比占用内存
4.1 list
from memory_profiler import profile
@profile
def test():
gen = [i for i in range(100000)]
print(type(gen))
if __name__ == '__main__':
test()
4.2 iterator
from memory_profiler import profile
@profile
def test():
gen = iter([i for i in range(100000)])
print(type(gen))
if __name__ == '__main__':
test()
4.3 generator
from memory_profiler import profile
@profile
def test():
gen = (i for i in range(100000))
print(type(gen))
if __name__ == '__main__':
test()