迭代器与生成器

Posted lynnlee

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了迭代器与生成器相关的知识,希望对你有一定的参考价值。

迭代器与生成器

一、迭代器

  迭代器可以理解为一种特殊的游标,是对循环遍历等一系列操作组成的一种抽象描述。而迭代器协议是程序的一种绑定关系,实现了该协议的对象称为可迭代对象。迭代器协议强调对象必须提供一个next或__next__()方法,并且执行该方法只有两种决策,要么返回迭代中的下一项,要么者引起一个StopIteration异常,以终止迭代。for循环的本质是循环所有对象,使用的一定是迭代器协议生成对象。因此for循环可以遍历所有的可迭代对象(字符串、列表、元组、字典、文件对象等)。既然如此,为什么我们定义一个序列的时候没有使用next方法呢?这是为什么呢?从理论上来讲,只有实现迭代器的对象才可称为可迭代对象。而我们在定义字符串、列表、元组、字典、文件对象的时候,本身没有给出next方法。从这种角度上来看,他们并没有遵循迭代器协议。但是平时我们为什么还是认为他们是可迭代对象呢?

  Python提供了一个可以让某种数据类型变为可迭代数据类型的方法,即让某种数据类型的对象直接调用__iter__()或iter()方法,此时我们再查看该数据类型的对象时就多出了next方法。下面如我们通过一个简单的实例来分析,我们使用字符窜调用__iter__()方法,然后使用可迭代对象调用next方法。

 string = "hello world"
        myiter = string.__iter__()
        print(myiter)
        print(myiter.__next__())  #输出:h
        print(myiter.__next__())  #输出:e
        print(myiter.__next__())  #输出:l
        print(myiter.__next__())  #输出:l
        print(myiter.__next__())  #输出:o
        print(myiter.__next__())  #输出:
        print(myiter.__next__())  #输出:w
        print(myiter.__next__())  #输出:o
        print(myiter.__next__())  #输出:r
        print(myiter.__next__())  #输出:l
        print(myiter.__next__())  #输出:d
        print(myiter.__next__())  #报错:StopIteration异常 

 结果证明next方法的确是在遍历字符窜,当next方法的遍历越过界时引发StopIteration异常。通过对这个实验的分析,发现for循环这个机制跟索引原来没有半毛钱关系,并且字符串、列表、元组、字典、文件对象本身不遵循迭代器协议,他们能被看作是可迭代对象的原因是因为for循环基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用对象的__iter__()方法将其转换成一个迭代器,然后用迭代器协议去实现循环访问。,直到for循环捕捉到StopIteration异常终止循环。当然我们也能理解了为什么for循环遍历字典的时候取到是key值,而不是value值,因为它被转化为可迭代对象后直接调用了next方法,而next方法得到的就是key值。包括文件的遍历也是一样的。可迭代对象的好处在于节省内存空间,用即取,不用不取。

 

二、生成器

  基于迭代器而实现的一种数据类型,这种数据类型自动实现了迭代器协议(其他数据类型需要自己调用内置的__iter__()方法),所以生成器本身就具有next方法。不需要通过调用__iter__()方法实现。Python有两种不同的方式提供生成器,下面将进行具体的介绍,当然笔者局限于知识水平不能全方位覆盖生成器的知识点。

 

1.生成器函数:

  常规的函数定义方式,使用yield关键字替代return关键字返回结果。yield关键字一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行。yield关键字相当于直接调用迭代器协议,对迭代器进行封装。它的第一个特性是相当于return,第二个特性是可以保留函数的运行状态。那如何保留函数的运行状态呢?我们可以将yield关键字穿插在函数想保留的状态之间。在某种特定的条件下使用next方法依次执行yield以下部分的程序。

        def test():
            yield "Yahoo"

        #调用
        g = test()
        print(g)               #输出:<generator object test at 0x000001B64793F7D8>
        print(g.__next__())    #输出:Yahoo

  

2. 生成器表达式:

   类似于列表推导式,它是一种三元运算表达式。生成器按需求返回结果对象,而不是一次性构建一个列表结果。与列表推导式的不同之处在于生成器表达式使用的是一对括号,并且生成器表达式相对于列表推导式更加节省内存。这是为什么呢?因为生成器表达式是基于迭代器而构造的。可迭代对象的好处在于节省内存空间,用即取,不用不取。同时我们也看到Python的创始人对C语言的内存管理是如此的精准。Python的很多内置函数都是基于迭代器生成的。比如map函数、sum函数等等。生成器的应用非常广泛,下面将结合简单的sum函数进行讲解。

 num = ("number: %s"%i for i in range(10))
        print(num.__next__())  #输出:number: 0
        print(num.__next__())  #输出:number: 1
        print(num.__next__())  #输出:number: 2
        print(num.__next__())  #输出:number: 3
        print(num.__next__())  #输出:number: 4

 

列表非常占用内存,因此我们可以用生成器来替代。Python中的很多内置函数是可迭代的,它们的参数一般传的都是iterable参数。所以如果我们使用生成器来构造一个计算结果,一般来说我们是要在构造语句外面加上一对括号,但是在可迭代的内置函数里面,这对括号可以省略。从上面的知识中我们知道这些内置函数会默认对可迭代参数进行for循环遍历,所以加不加括号是无所谓的。下面程序的输出结果是等效的。

print(sum([1,2,3,4,5,6,7,8,9]))      #使用列表
        print(sum(i for i in range(10)))     #使用生成器
        print(sum(i for i in range(10)))     #去掉括号
        print(sum( range(10) ))              #等效替换
 

  

3.生成器的总结

  生成器函数和普通函数几乎一样。他们之间的差别在于生成器函数使用的是yield关键字返回一个值,而常规函数使用return返回一个值;yield关键字挂起该函数的状态,保留足够的信息,以便之后从它离开的地方继续执行。Python会自动实现迭代器,以便应用到迭代背景中(如很多内置函数)。由于生成器自动实现了迭代器协议。所以,我们可以调用它的next方法,并且在没有值可以返回的时候,生成器自动产生StopIteration异常;生成器的另一个重点知识是生成器只能遍历一次,并且产生生成器的方式除了使用生成器函数的方式外,一个括号也可以产生生成器。

    #简单的生成器函数
    def test():
        for i in range(10):
            yield i


    t = test()               #调用
    for i in t:              #第一遍遍历
        print(i)             #输出列表的内容     
    tt = (i for i in t)      #第二遍遍历
    print(list(tt))          #输出:[ ]

  

#括号产生生成器
    def test():
        for i in range(10):
            yield i


    t = test()            #调用
    t1 = (i for i in t)    
    t2 = (i for i in t1)
    print(list(t1))       #输出列表的内容
    print(list(t2))       #输出:[ ]
    """
    这里一直都有一个误区,其实t1和t2都没有任何值,只有在使用内置函数list,list自动
    调用next函数的时候,t1的遍开始,于是值才会产生。遍历完成后,由于只能遍历一次,
    值都被t1拿走了,t2是取不到值的,因此t2是一个空列表。
    
    """

  

4.生成器的优点

  生成器的好处之一是延迟计算,一次返回一个结果。换言之,它不会一次生成所有的计算结果,这对于大型数据处理非常有用。对内存的开销比较小。生成器的另一个好处是提高代码的可读性。Python编程的思想是在可读性的前提下提炼代码。这一点是Python编程所必须掌握的。下面将应用以上知识模拟单线程并发处理,即生产者与消费者模型。代码如下:

 

    import time
    def consumer(name):
        print("【%s】准备吃饺子"%name)
        while True:
            dumping = yield
            time.sleep(1)
            print("【%s】吃掉%s饺子了"%(name, dumping))

    def producer():
        p1 = consumer("Lily")
        p2 = consumer("Jiony")
        p1.__next__()
        p2.__next__()
        for i in range(10):
            time.sleep(1)
            p1.send(i)
            p2.send(i)

    #调用        
    producer()

  

 

以上是关于迭代器与生成器的主要内容,如果未能解决你的问题,请参考以下文章

迭代器与生成器

迭代器与生成器

python3 迭代器与生成器

Python3 迭代器与生成器

Python的迭代器与生成器

迭代器与生成器