【自动化运维新手村】番外-迭代器生成器

【摘要】

在之前的文章中我们提到过很多次的可迭代对象,并且很多面试中对于Python几乎必问的一个问题是:“请解释一下迭代器和生成器?”

在Python中对象是否可迭代是一个十分重要的特性,今天我们就一次性的给大家讲解清楚究竟什么是可迭代,如何判断一个对象是否的可迭代的,什么又是迭代器,以及什么是生成器

下图可以清晰的表示可迭代对象迭代器生成器之间的关系:

在这里插入图片描述

【迭代(iter)】

迭代就是指更新换代的过程,要重复进行,而且每次的迭代都必须基于上一次的结果。

我们使用for循环的时候就是把元素从容器里一个个取出来,这种过程其实就是迭代。

【可迭代对象(Iterable)】

可迭代对象的概念比迭代器要广很多,从上图可以看到,可迭代对象包括迭代器,而生成器又是一种特殊的迭代器。

通俗的讲可以通过for循环遍历的对象都是可迭代对象

准确的讲,一个实现了__iter__()`方法的对象就是可迭代对象,所以我们判断一个对象是不是可迭代,有两种方法:

  1. 通过dir函数获取某个对象的所有属性和方法,只要这个对象实现了__iter()__方法,那么它就是可迭代对象, 如下:
dir([1, 2, 3])
# 输出 [..., '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__',...]
  1. 通过isinstance()函数,判断一个对象是否是Iterable类型
from collections import Iterable
print(isinstance([1, 2, 3], Iterable))
# 输出 True

既然可以被for循环遍历的对象就是可迭代对象,那是否可迭代对象都可以通过for循环遍历呢?

现在可以自己定义一个可迭代对象做一下验证

from collections import Iterable
class MyIter:
    def __iter__(self):
        pass

my_iter = MyIter()
print(isinstance(my_iter, Iterable))
# 输出 True
for i in my_iter:
    pass
# 输出 TypeError: iter() returned non-iterator of type 'NoneType'

上述代码证明不是所有的可迭代对象都可以被for循环遍历,前提是必须正确的实现__iter__()方法,让可迭代对象返回一个迭代器。

所以看到这里大家可以认为,for循环遍历的对象必须是一个可以返回迭代器的可迭代对象

【迭代器对象(iterator)】

上面已经介绍过,一个可迭代对象要正确的实现__iter__()方法让其返回一个迭代器,最终才可以被for循环遍历。

那么究竟要返回一个什么样的东西才叫迭代器呢?

官方定义如下:

  1. 迭代器是一个对象,该对象代表了一个数据流。
  2. 重复调用迭代器的__next__方法(或将迭代器对象当作参数传入內置函数next()中)将依次返回数据流中的元素。
  3. 当数据流中无可返回元素时,则抛出StopIteration异常。
  4. 迭代器必须拥有__iter__方法,该方法返回迭代器对象自身

通俗的讲就是迭代器对象必须同时实现__iter__()方法和__next()__方法。

for循环在遍历一个可迭代对象的时候,底层机制为

  1. 先调用其__iter__()方法获取到其对应的迭代器对象;

  2. 并不断调用迭代器对象的__next__()方法获取下一个元素;

  3. 最终遇到StopIteration异常结束

上面的代码等价于:

my_iter = iter(my_list)
while True:
    try:
        print(next(my_iter))
    except StopIteration:
        break
# 输出 1, 2, 3
# iter(my_iter) 等价于 my_list.__iter__()
# next(my_iter) 等价于 my_iter.__next__()

示例

这里有一个重要的问题需要大家思考一下:列表可以被for循环遍历,那列表是不是迭代器呢?

# for循环遍历list的代码如下:
my_list = [1, 2, 3]
for i in my_list:
    print(i)
# 输出 1, 2, 3

按照官方的迭代器的定义,我们只需要做如下操作就可以验证列表是不是迭代器:

my_list = [1, 2, 3]
next(my_list)
# 输出 TypeError: 'list' object is not an iterator

答案很明显,list并不是一个迭代器,我们进一步看看list具有哪些属性和方法:

dir(my_list)
# 输出 ['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

可以很清楚的看到,list并没有实现__next__()方法,所以它不是一个迭代器。

在使用for循环对list进行遍历的时候,是Python底层调用了iter()方法,帮我们获取到了list对应的迭代器,如下:

print(iter(my_list))
# 输出 <list_iterator object at 0x7fccb81ca7c0>
print(my_list.__iter__())
# 输出 <list_iterator object at 0x7fccb81ca7c0>

那么list对应的迭代器又实现了哪些属性和方法呢:

dir(iter(my_list))
# 输出 ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']

显而易见,list对应的迭代器对象实现了__next__()方法,所以我们可以验证一下:

>>> my_iter = my_list.__iter__()
>>> my_iter.__next__()
1
>>> my_iter.__next__()
2
>>> my_iter.__next__()
3
>>> my_iter.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

从上述代码可以看出:

  • 我们可以对my_list通过__iter__()返回的迭代器对象执行__next__()方法
  • 并且每次对my_iter执行__next__()方法后,my_iter都会发生变化,但my_list却是不变的

综上,for循环遍历list其实本质在做的事情如下:

my_iter = iter(my_list)
while True:
    try:
        print(next(my_iter))
    except StopIteration:
        break
# 输出 1, 2, 3
# iter(my_iter) 等价于 my_list.__iter__()
# next(my_iter) 等价于 my_iter.__next__()

所以最终可以得出的结论是:

  1. for循环遍历的并不是可迭代对象本身,而是其对应的迭代器对象

  2. 可迭代对象对应的迭代器对象遍历结束后就会失效,无法回到起始位置

  3. 可以多次对可迭代对象进行遍历,是因为每次都会调用iter()方法生成该可迭代对象的新的迭代器对象

迭代器最大的好处就是节省内存

在Python中,文件对象也是一个迭代器,我们可以做如下验证:

>>> f = open("test.txt")
>>> dir(f)
['_CHUNK_SIZE', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', '_finalizing', 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'read', 'readable', 'readline', 'readlines', 'reconfigure', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'write_through', 'writelines']
>>> type(f)
<class '_io.TextIOWrapper'>
>>> from collections import Iterator
>>> isinstance(f, Iterator)
True

既然文件对象也是一个迭代器,那我们假设一个场景,此时有一个很大的文本文件需要我们读取,常规的做法是

with open("test.txt") as f:
    data = f.readlines()
for line in data:
     pass

上述代码可以讲文件内容全部读取出来,并按行保存为数组赋值给data,那么可想而知当文件内容很多时,data就会占用很大的内存。

但我们可以运用迭代器的性质来很好的做出优化:

with open("test.txt") as f:
		for line in f:
     	  pass

因为文件对象f是一个迭代器,所以理所当然可以对其使用for循环遍历。

而for循环遍历时第一步就是调用iter()函数获取到f到迭代器对象(此处f到迭代器对象就是自身),接下来就是每次循环的时候调用__next__()函数来获取下一行。

所以利用迭代器可以很大程度的节省内存,只有在调用next()方法时才会去获取下一个元素,这样可以避免一次性的加载很大的对象导致占用内存过多,而是在需要时才进行惰性计算

【生成器对象(Generator)】

从开头的图中大家已经可以看出来,生成器是一种特殊的迭代器。

至于为什么叫生成器?

主要是因为生成器函数并没有一次性将所有的元素都返回,而是每调用__next__()方法一次,生成一个元素,顾名思义叫做生成器。

那这不就是迭代器吗?

确实生成器就是一种迭代器,它具有迭代器所有的特点,只不过在Python中更轻量,更优雅,生成器主要有两种形式:

  1. 通过函数创建,称作generator function

  2. 通过推导式创建,例如g=(x*2 for x in range(10)),称作generator expression

生成器对象是一种特殊的Iterator函数,它会在执行过程中保存执行的上下文环境,并在下次循环中从yield语句后继续执行,

生成器的标志就是yield关键字

所以生成器和迭代器最大的区别就是:

迭代器是一种宽泛的概念,大家可以将其理解为一种抽象的描述,平时我们并不会感知到,因为大多数的迭代器都有Python内置方法帮我们实现了,而生成器就是平时编程过程中经常会使用到的一种迭代器而已。

【示例】

这里给大家分别用普通方法,迭代器以及生成器三种方式实现求解斐波那契数列,并遍历从1到100000点结果:

传统方式:
def fibonacci_fun(num):
    numlist = [0,1]
    for i in range(num-1):
        numlist.append(numlist[-2]+numlist[-1])
    return numlist[1:]
  
res = fibonacci_fun(100000)
for i in res:
    print(i)
迭代器:
class Fibonacci_Iterator:
    def __init__(self,counts):
        self.start = 0
        self.end = 1
        self.counts = counts
        self.time = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.time >= self.counts:
            raise StopIteration
        else:
            self.time += 1
            self.start,self.end = self.end,self.start + self.end
            return self.start
          
f_iter = Fibonacci_Iterator(100000)
for i in f_iter:
     print(i)
生成器:
def fibonacci_generator(counts):
    start = 0
    end = 1
    for _ in range(counts):
        start,end = end,end+start
        yield start

f_gene = fibonacci_generator(100000)
for i in f_gene:
     print(i)

大家可以从上面的代码中看出:

  1. 传统方法求遍历解时需要一次性算出所有的元素,并将其保存在一个列表中返回;

  2. 迭代器求解遍历时只会在for循环每次隐式调用__next__()方法时才求解;

  3. 生成器的实现则最为优雅,代码执行到yield会暂停,把结果返回出来,再次隐式调用__next__()启动生成器的时候会在暂停的位置继续往下执行

【总结】

关于可迭代对象,迭代器,生成器的讲解在我看来是刚接触Python时最为难理解的部分,这篇文章也只是较为粗浅给大家讲解,相信大家会在后续的编程中会对今天的概念有更为深刻的理解和运用。


在这里插入图片描述

欢迎大家添加我的个人公众号【Python玩转自动化运维】加入读者交流群,获取更多干货内容

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值