关于Python中迭代、迭代器、迭代对象以及生成器的理解

容器

什么是容器数据类型?

在某些对象中包含对其他对象的引用,这些对象就是容器。

简单来说,容器是用来存储元素的的一种数据结构,容器将所有数据存放在内存中。

Python标准内建容器:list, dict, set, tuple

但是range不是容器数据类型!

迭代概念

  1. 什么是迭代
  2. 什么是迭代对象
  3. 什么是迭代器

迭代

在计算机科学中,迭代是程序中对一组指令(或一定步骤)的重复.—-维基百科

在Python中我理解的迭代的概念是遍历,遍历一个容器类型对象(字符串,列表,字典)。在知乎中看到一个说法关于迭代和递归的区别:”迭代是将输出作为输入,再次进行处理”,感觉有个比较恰当的例子,机关枪扣动班级之后发射子弹,利用后坐力继续扣动扳机,递归就是自己调用自己。

关于递归与迭代的区别,知乎上还有一个回答挺好:”递归是自己调用自己,每次旨在缩小问题规模。迭代是自己执行很多次,每次旨在更接近目标。”

一个例子讲的明白一点:

你想给你女朋友买礼物,但是你不知道你女朋友喜欢什么,怎么办?

递归: 你先找你女朋友的好朋友,问她怎么解决,她也不知道,她就去找你女朋友的好闺蜜。解决了

迭代: 你一件一件的挑选,挑选到合适的礼物

Python中可以用for循环实现迭代的过程:

for char in "qweqwe":
    print char

但是并非所有对象都可以用for, 比如数字,原因是字符串是一个序列,但是数字却不是。

迭代对象

迭代对象: 可以返回一个迭代器的__iter__方法,或者可以支持下标索引的__getitem__方法

dir(1)  #没有__iter__方法
dir("123") #有__iter__方法,也有__getitem__方法

判断对象是否有__iter__方法:

  • dir()

  • isinstance():

    import collections
    isinstance(2, collections.Iterable)     # False
    isinstance("abc", collections.Iterable) # True
    
  • hasattr()
    hasattr(2,"__iter__")     # False
    hasattr("123","__iter__") # True
    
  • 用iter()查看是否报错
    iter(2)     # 报错:'int' object is not iterable
    iter("123") # <str_iterator at 0x1e2396d8f28>
    

注意: 关于iter()方法说明,是Python内置方法,作用就是将迭代对象变成迭代器。所以说明两点:

  1. 迭代对象能变成迭代器
  2. 迭代对象和迭代器是两个不同的东西

迭代器

迭代器: 任意对象,实现__iter__ ,同时实现next(python2)或者__next__方法,就是一个迭代器。

迭代器与迭代对象的区别:

一同两不同:

  1. 一同: 两者都含有__iter__, 都有迭代的方法
  2. 两不同: 迭代对象转为迭代的时候,丢失了__getitem__ 的方法,多了 __next__ 方法。

__next__方法:

  1. 无需借助外部方法(for循环等),即可实现自我迭代,自我循环的过程。

  2. 自我遍历的实现,同时存在特殊性——单路循环。简单说,只能循环一次,一旦循环完毕,则生存周期结束。

迭代器与迭代对象之间的区别:

举个例子:迭代对象就像是98K狙击枪,要用手拉才能上膛(外部方法实现循环),子弹没有了,还可以重新状态子弹。迭代器就是是电击枪(那种飞出去两个镖,后面有线,打到人身上能电晕人的那种),无需手动 上膛(自我循环),但是射出去之后,不能更换子弹。

简单来说:

  1. 迭代对象是需要外部循环,可以反复使用。
  2. 迭代器是自我循环,但是是一次性的。

注意: range不是迭代器,是迭代对象

生成器

什么是生成器(generator)

如果通过迭代对象的容量过大的时候,不仅占用内存过大 ,而且如果仅仅使用几个元素的时候,那么占用的空间都会被浪费。

所以如果可以用计算的方式,不断地推到出来后面的元素,实现一遍计算一遍循环,这种机制就成为——生成器。

生成器是一种 更加高级,更加优雅的迭代器。

生成器的作用

使用生成器,可以节约内存。不必创建完整的数据,一边循环一边计算。

一句话:我要计算超大数据,但是我又想不找用那么多内存。

如何创建生成器

  1. 把一个列表生成式的[] 改成()

    L =[x * x for in range(10)]

==>G = (x * x for in range(10))

  1. 使用普通函数,但是包含yield关键字

    使用yield关键字,则这个函数不是一个普通的函数了,而是一个generator。调用这个函数的时候,就会创建一个生成器对象。

生成器的原理

  1. 生成器(generator)能够迭代的原因是它有next()方法,工作原理就是反复调用next()方法,直到出现异常

  2. 带有yield的方法不是一个普通的方法,而是一个generator

    1. 可用next()调用生成器取值
    2. 可用for循环来遍历取值

    (基本上不会用next()来获取下一个返回值,而是直接使用for循环来迭代)。

  3. yield相当于return一个值,并且记住返回的这个位置。下次迭代的时候,是从yield下面的一行开始执行的,并不是重头开始的。

总结:

生成器仅仅保存了一套生成数值的算法,并且没有让这个算法现在就开始执行,而是我什么时候调它,它什么时候开始计算一个新的值,并给你返回。

思考:

为什么迭代对象转换迭代器的时候丢失了__getitem__方法?

这个__getitem__方法是干什么用的?

官方定义(翻译):

__getitem__() 方法用于返回参数 key 所对应的值,这个 key 可以是整型数值和切片对象,并且支持负数索引;如果 key 不是以上两种类型,就会抛 TypeError;如果索引越界,会抛 IndexError ;如果定义的是映射类型,当 key 参数不是其对象的键值时,则会抛 KeyError 。

也就是说之所以迭代对象可以支持切片,就是因为支持__getitem__ 这个方法,是通过这个方法实现的。也就是说转换的过程中丢失了切片的操作,为什么丢失?

因为: 迭代器是一次性的,具有消耗性。也就是说长度是变化的,所以如果支持切片,每次切片出来的数据是不一样的。

为什么迭代对象转换迭代器的时候增加了__next__ 方法

增加了__item__方法的结果,就是迭代器能实现了自我循环。

容器、迭代对象、迭代器、生成器的关系

  1. 生成器一定是迭代器
  2. 迭代器一定是迭代对象
  3. 迭代对象可以变成迭代器,通过iter()可以转换
  4. 迭代对象有__iter__ ,或者__getitem__方法
  5. 迭代器有__next__方法
  6. 大部分容器是迭代对象,迭代对象不一定是容器,比如文件对象,管道对象