迭代对象、迭代器、生成器浅析
这三者都是迭代对象,可通过for循环逐个获取对象元素。生成器基本不占用内存无论有多大数据量,但是只能使用一次(也可以通过一些途径使用多次)。
迭代对象 iterables
能一次返回一个元素的对象,主要用于for
循环。
基本上python的所有容器(container)都是可迭代的,比如有序容器list, str, tuple,set 和无序容器dict, file,还有collections模块提供的几个亮瞎眼的容器namedtuple、OrderedDict、Counter、defaultdict、deque、ChainMap、UserDict、UserList、UserString。一般情况下,要扩展内置类型的功能需要通过子类化来实现,但轮子已被前辈造好,不仅性能好,而且使得代码更加简洁pythonic。
例子,Counter()用于统计元素出现的个数,是字典builtins.dict的子类。
通常做法:
s="afmldskmfl"
occurrences = {}
for _,e in enumerate(s):
occurrences1[e]=occurrences1.get(e,0)+1#返回{';': 1, 'a': 3, 'd': 1, 'f': 1, 'k': 1, 'l': 1, 's': 3}
pothonic写法:
occurrences = Counter(s)#返回Counter({';': 1, 'a': 3, 'd': 1, 'f': 1, 'k': 1, 'l': 1, 's': 3})
occurrences.most_common(2)#返回[('s', 3), ('a', 3)]
Counter()除了继承dict的方法,自己又实现了三个,elements()、most_common()、subtract()。
可迭代对象本质上是实现了魔法方法__iter__()
或__getitem__()
,该方法可以看做是所有迭代对象都要遵循的一个协议。所以,除了python内置的迭代对象,我们也可以在自己定义的class里通过重载__iter__()
来获得可迭代性。
大多容器是可迭代对象,只有少数是迭代器。比如list
就是个迭代对象,但如果调用了魔法方法__iter__()
,则会变成一个迭代器。例子如下。
x = [1,2,3]
isinstance(x, clc.abc.Iterable)#返回True
isinstance(x, clc.abc.Iterator)#返回False
x1 = iter(x)#或者用x1 = x.__iter__()
isinstance(x1, clc.abc.Iterator)#返回True
迭代器 iterator
迭代器必定是可迭代对象。
所以迭代器是可迭代对象的子类,必须遵循迭代协议,即有方法__iter__()
,同时自己也有方法__next__()
。
其实,__next__()
基本是实现了迭代对象的for循环操作,但区别是next
会耗尽迭代器,当到达元素末尾之后再调用next会出现StopIteration
异常,同时也是一次性的,耗尽之后再没法使用。
而for循环接收迭代对象的StopIteration
异常并以此为break条件,同时,for中使用迭代对象就完全可以工作了,没必要把迭代器放在for循环里,多此一举,而且最重要的是迭代对象不会耗尽。
一个迭代对象可通过魔法方法__iter__()
转变为一个迭代器,即iterator=iterable.__iter__()
或iterator=iter(iterable)
。
自己实现一个计算Fibonacci数列的迭代器:
import collections as clc
class Fibonacci(object):
def __init__(self, n=10):
self.a = 0
self.b = 1
self.n = n
def __iter__(self):
"""若存在此方法,python解释器会把实例化的对象当做迭代对象"""
return self
def __next__(self):
"""若存在此方法,python解释器会把实例化的对象当做迭代器"""
if self.n == 0:
raise StopIteration
self.a, self.b = self.b, self.a+self.b
self.n -= 1
return self.b
if __name__ == "__main__":
z = Fibonacci(5)#实例化对象
print(isinstance(z, clc.abc.Iterable))#输出True
print(isinstance(z, clc.abc.Iterator))#输出True
print(next(z))#输出1
print(z.__next__())#输出2
for f in z:
print(f)#输出3,5,8
虽然这不是实现Fibonacci数列很Pythonic的方式,但是可以了解迭代器的工作原理。
迭代器可以说是个鸡肋般的存在,远不如迭代对象用途广泛,近不如生成器那样高效简洁—-鲁迅。但他是生成器的基石。
生成器 generator
为什么叫生成器呢?其实是一种生成的迭代器(generate iterators),故也称generator iterators,简称generators,即生成器。
一个自定义生成器最显著的特征是yield
语句。当解释器看到存在yield
语句时,便知道这是个生成器定义。
普通函数用return
返回,而生成器用yield
作为名义上的返回。
当调用普通函数时,return
语句前的所有代码全部被执行,然后返回return
后的值;如果返回的是一个list
,则所有值都会写入这个list
存在于内存中。
当调用生成器时,每次调用会返回yield
后面的值并把当前位置记住,下次再调用时就从这个位置开始。所以,yield
语句可以写在一个无限循环里而不用提供终止函数的条件!因为每次返回一个值,所以基本不会占用内存,无论数据有多少。
其实,yield
实现的功能与__iter__()
+__next__()
是一样的。很显然,要实现同一个功能,定义一个类比直接用一个普通函数(plain function)更复杂。而yield
正好可以通过一个简单函数实现一个类才能干的事。
用生成器实现Fibonacci数列。
def fibonacci(n):
a, b = 0, 1
while True:
if n == 0:
raise StopIteration
yield b
a, b = b, a+b
n -= 1
if __name__ == "__main__":
z = fibonacci(5)#调用函数,将生成器赋给变量z
print(isinstance(z, clc.abc.Iterable))#输出True
print(isinstance(z, clc.abc.Iterator))#输出True
print(next(z))#输出1
print(z.__next__())#输出2
for f in z:
print(f)#输出3,5,8
python标准库里自带的生成器有range()
、dict.items()
、zip()
,map()
、open
等,功能都很强度,尤其是zip
对于一次性迭代多个迭代对象时更是效率惊人,可参照我的前一篇文章8行代码实现ui文件到py文件转换。
总结
从范围来讲,迭代对象>迭代器>生成器
网上的一张图片很清晰地解释了三者之间的关系。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(内容同步更新到微信公众号python数学物理,微信号python_math_physics)