• 迭代器(iterator)


    Date: 2019-05-23

    Author: Sun

    为何要引入迭代器?

    ​ 通过列表生成式,我们可以直接创建一个列表,但是,受到内存限制,列表容量肯定是有限的,而且创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

    ​ 我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间,在Python中,这种一边循环一边计算的机制,称为生成器:generator

    1.1 什么迭代器呢?

    ​ 在Python中如果一个对象有__iter__( )方法和__next__( )方法,则称这个对象是迭代器(Iterator);其中__iter__( )方法是让对象可以用for ... in循环遍历,next( )方法是让对象可以通过next(实例名)访问下一个元素。

    ​ 注意:这两个方法必须同时具备,才能称之为迭代器。

    ​ 迭代器是在python2.2中被加入的,它为类序列对象提供了一个类序列的接口。有了迭代器可以迭代一个不是序列的对象,因为他表现出了序列的行为。

    ​ 迭代器的实质是实现了next()方法的对象,常见的元组、列表、字典都是迭代器。

    迭代器中重点关注两种方法:

    __iter__方法:返回迭代器自身。可以通过python内建函数iter()调用。

    __next__方法:当next方法被调用的时候,迭代器会返回它的下一个值,如果next方法被调用,但迭代器没有只可以返回,就会引发一个StopIteration异常。该方法可以通过 python 内建函数next()调用。

    举例

    内建函数iter()可以从可迭代对象中获得迭代器。

    >>> it = iter([1,2,3])
    >>> next(it)
    1
    >>> next(it)
    2
    >>> next(it)
    3
    >>> next(it)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    >>> 
    

    ​ 迭代器是一个带状态的对象,他能在你调用next()方法的时候返回容器中的下一个值,任何实现了__iter____next__()(python2中实现next())方法的对象都是迭代器。

    __iter__返回迭代器自身,__next__返回容器中的下一个值,如果容器中没有更多元素了,则抛出StopIteration异常,至于它们到底是如何实现的这并不重要。

    ​ 迭代器就是实现了工厂模式的对象,它在你每次你询问要下一个值的时候给你返回。

    1.2 如何创建一个迭代器

    要创建一个迭代器有2种方法,其中前两种分别是:

    1. 为容器对象添加 __iter__()__next__() 方法(Python 2.7 中是 next());__iter__() 返回迭代器对象本身 self__next__() 则返回每次调用 next() 或迭代时的元素;
    2. 内置函数 iter() 将可迭代对象转化为迭代器

    案例:创建迭代器容器

    # Create iterator Object
    class Container:
       def __init__(self, start=0, end=0):
          self.start = start
          self.end = end
    
       def __iter__(self):
          print("I made this iterator!")
          return self
    
       def __next__(self):
          print("Calling __next__ method!")
          if self.start < self.end:
             i = self.start
             self.start += 1
             return i
          else:
             raise StopIteration()
    
    
    c = Container(0, 5)
    for i in c:
       print(i)
    

    ​ 对于迭代器对象,使用for循环遍历整个数组其实是个语法糖,他的内部实现还是通过调用对象的__next__()方法。

    ​ 创建迭代器对象的好处是当序列长度很大时,可以减少内存消耗,因为每次只需要记录一个值即刻(经常看到人们介绍 Python 2.7 的 range 函数时,建议当长度太大时用 xrange 更快,在 Python 3.5 中已经去除了 xrange 只有一个类似迭代器一样的 range)。

    1.3 斐波那契数列应用举例

    我们如果采用正常的斐波那契数列求值过程如下:

    def fibs(n):
       if n == 0 or n == 1:
          return 1
       else:
          return fibs(n-1) + fibs(n-2)
    
    
    print([fibs(i) for i in range(10)])
    
    

    自定义一个迭代器, 实现斐波那契数列

    class Fib2(object):
    	def __init__(self, n):
    		self.a = 0
    		self.b = 1
    		self.n = n
    		self.count = 0
    
    	def __iter__(self):
    		return self
    
    	def next(self):
    		res = self.a
    		self.a, self.b = self.b, self.a + self.b
    		if self.count > self.n:
    			raise StopIteration
    		self.count += 1
    		return res
    
    print(list(Fib2(10)))
    
    其他迭代器案例

    (1) 有很多关于迭代器的例子,比如itertools函数返回的都是迭代器对象。

    def test_endless_iter():
       '''
       生成无限序列
       :return:
       '''
       from itertools import count
       counter = count(start=3)
       print(next(counter))
       print(next(counter))
       print(next(counter))  
    

    (2) 从一个有限序列中生成无限序列

    def test_cycle_iter():
       '''
       从一个有限序列中生成无限序列
       :return:
       '''
       from itertools import cycle
       colors = cycle(['red', 'white', 'blue'])
       print(next(colors))
       print(next(colors))
       print(next(colors))
       print(next(colors))
    

    总结:

    ​ 对于list、string、tuple、dict等这些容器对象,使用for循环遍历是很方便的。在后台for语句对容器对象调用iter()函数。iter()是python内置函数。
    iter()函数会返回一个定义了next()方法的迭代器对象,它在容器中逐个访问容器内的元素。next()也是python内置函数。在没有后续元素时,next()会抛出一个StopIteration异常,通知for语句循环结束。

    ​ 迭代器是用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用next()函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。实际上,在使用next()函数的时候,调用的就是迭代器对象的next方法(Python3中是对象的next方法,Python2中是对象的next()方法)。所以,我们要想构造一个迭代器,就要实现它的next方法。但这还不够,python要求迭代器本身也是可迭代的,所以我们还要为迭代器实现iter方法,而iter方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的iter方法返回自身self即可。

    2. 生成器

    ​ 生成器是一个特殊的程序,可以被用作控制循环的迭代行为,python中生成器是迭代器的一种,使用yield返回值函数,每次调用yield会暂停,而可以使用next()函数和send()函数恢复生成器。

    生成器作用?

    ​ 延迟操作。也就是在需要的时候才产生结果,不是立即产生结果。

    什么是生成器?

    ​ 生成器类似于返回值为数组的一个函数,这个函数可以接受参数,可以被调用,但是,不同于一般的函数会一次性返回包括了所有数值的数组,生成器一次只能产生一个值,这样消耗的内存数量将大大减小,而且允许调用函数可以很快的处理前几个返回值,因此生成器看起来像是一个函数,但是表现得却像是迭代器。

    ​ 生成器是包含yield关键字的函数。本质上来说,关键字yield是一个语法糖,内部实现支持了迭代器协议,同时yield内部是一个状态机,维护着挂起和继续的状态。

    python中的生成器

    ​ 要创建一个generator,有很多种方法,第一种方法很简单,只有把一个列表生成式的[]中括号改为()小括号,就创建一个generator

    ​ 举例如下:

    # 列表生成式
    lis = [x * x for x in range(10)]
    print(lis)
    # 生成器
    generator_ex = (x * x for x in range(10))
    print(generator_ex)
    
    结果:
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    < generator
    object < genexpr > at
    0x000002A4CBF9EBA0 >
    
    #生成器
    generator_ex = (x*x for x in range(10))
    print(next(generator_ex))
    print(next(generator_ex))
    print(next(generator_ex))
    print(next(generator_ex))
    
    
    #生成器, 可以直接循环得出结果
    generator_ex = (x*x for x in range(10))
    for i in generator_ex:
        print(i)
         
    结果:
    0
    1
    4
    9
    
    生成器调用顺序

    那么,生成器是怎么调用执行的呢?只需要了解下面几条规则即可:

    a. 当生成器被调用的时候,函数体的代码不会被执行,而是会返回一个迭代器,其实,生成器函数返回生成器的迭代器。 “生成器的迭代器”这个术语通常被称作”生成器”。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法,其中一个就是next()。如同迭代器一样,我们可以使用next()函数来获取下一个值。需要明白的是,这一切都是在yield内部实现的。

    b. 当next()方法第一次被调用的时候,生成器函数才开始执行,执行到yield语句处停止
    next()方法的返回值就是yield语句处的参数(yielded value)
    当继续调用next()方法的时候,函数将接着上一次停止的yield语句处继续执行,并到下一个yield处停止;如果后面没有yield就抛出StopIteration异常。

    c.每调用一次生成器的next()方法,就会执行生成器中的代码,知道遇到一个yield或者return语句。yield语句意味着应该生成一个值(在上面已经解释清楚)。return意味着生成器要停止执行,不在产生任何东西。

    d. 生成器的编写方法和函数定义类似,只是在return的地方改为yield。生成器中可以有多个yield。当生成器遇到一个yield时,会暂停运行生成器,返回yield后面的值。当再次调用生成器的时候,会从刚才暂停的地方继续运行,直到下一个yield。生成器自身又构成一个循环器,每次循环使用一个yield返回的值。

    注意:

    (1)生成器是只能遍历一次的。

    (2)生成器是一类特殊的迭代器。

    分类:

    第一类:生成器函数:

    生成器函数:也是用def定义的,利用关键字yield一次性返回一个结果,阻塞,重新开始

    ​ 还是使用 def 定义函数,但是,使用yield而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行。

    如下案例加以说明:

    def Fib3(max):
       n, a, b = 0, 0, 1
       while n < max:
          yield b
          a, b = b, a + b
          n = n + 1
       return '亲!没有数据了...'
    
    
    print("@@@@@@@@@@@@@222")
    # 调用方法,生成出10个数来
    f=Fib(10)
    # 使用一个循环捕获最后return 返回的值,保存在异常StopIteration的value中
    while  True:
       try:
          x=next(f)
          print("f:",x)
       except StopIteration as e:
          print("生成器最后的返回值是:",e.value)
          break
    

    第二类:生成器表达式:

    ​ 类似于列表推导,只不过是把一对大括号[]变换为一对小括号()。但是,生成器表达式是按需产生一个生成器结果对象,要想拿到每一个元素,就需要循环遍历。

    如下案例加以说明:

    # 一个列表
    xiaoke=[2,3,4,5]
    # 生成器generator,类似于list,但是是把[]改为()
    gen=(a for a  in xiaoke)
    for  i  in gen:
        print(i)
    #结果是:
    2
    3
    4
    5
    
    

    ​ 为什么要使用生成器?因为效率。使用生成器表达式取代列表推导式可以同时节省 cpu 和 内存(RAM)。如果你构造一个列表(list)的目的仅仅是传递给别的函数, 比如 传递给tuple()或者set(), 那就用生成器表达式替代吧!

    iter

    如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的next()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

    我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:

    class Fib(object):
        def __init__(self):
            self.a, self.b = 0, 1 # 初始化两个计数器a,b
    
        def __iter__(self):
            return self # 实例本身就是迭代对象,故返回自己
    
        def next(self):
            self.a, self.b = self.b, self.a + self.b # 计算下一个值
            if self.a > 100000: # 退出循环的条件
                raise StopIteration();
            return self.a # 返回下一个值
    
    

    现在,试试把Fib实例作用于for循环:

    >>> for n in Fib():
    ...     print n
    ...
    1
    1
    2
    3
    5
    ...
    46368
    75025
    
    

    getitem

    要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:

    class Fib(object):
        def __getitem__(self, n):
            a, b = 1, 1
            for x in range(n):
                a, b = b, a + b
            return a
    
    

    现在,就可以按下标访问数列的任意一项了:

    >>> f = Fib()
    >>> f[0]
    1
    >>> f[1]
    1
    >>> f[2]
    2
    >>> f[3]
    3
    >>> f[10]
    89
    >>> f[100]
    573147844013817084101
    
    

    但是list有个神奇的切片方法:

    >>> range(100)[5:10]
    [5, 6, 7, 8, 9]
    
    

    对于Fib却报错。原因是__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:

    class Fib(object):
        def __getitem__(self, n):
            if isinstance(n, int):
                a, b = 1, 1
                for x in range(n):
                    a, b = b, a + b
                return a
            if isinstance(n, slice):
                start = n.start
                stop = n.stop
                a, b = 1, 1
                L = []
                for x in range(stop):
                    if x >= start:
                        L.append(a)
                    a, b = b, a + b
                return L
    
    

    现在试试Fib的切片:

    >>> f = Fib()
    >>> f[0:5]
    [1, 1, 2, 3, 5]
    >>> f[:10]
    [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    
    

    但是没有对step参数作处理:

    >>> f[:10:2]
    [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
    
    
    

    也没有对负数作处理,所以,要正确实现一个__getitem__()还是有很多工作要做的。

    文件是可迭代对象,也是迭代器

    f = open('housing.csv')
    from collections import Iterator
    from collections import Iterable
    
    print(isinstance(f, Iterator))     #判断是不是迭代器
    print(isinstance(f, Iterable))     #判断是不是可迭代对象
    
    True
    True
    
    

    小结:

    • 凡是可作用于for循环的对象都是Iterable类型;
    • 凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
    • 集合数据类型如listdictstr等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。

    Python3的for循环本质上就是通过不断调用next()函数实现的,例如:

    for x in [1, 2, 3, 4, 5]:
        pass
    
    

    实际上完全等价于

    # 首先获得Iterator对象:
    it = iter([1, 2, 3, 4, 5])
    # 循环:
    while True:
        try:
            # 获得下一个值:
            x = next(it)
        except StopIteration:
            # 遇到StopIteration就退出循环
            break
    
    

    yield的总结

      (1)通常的for..in...循环中,in后面是一个数组,这个数组就是一个可迭代对象,类似的还有链表,字符串,文件。他可以是a = [1,2,3],也可以是a = [x*x for x in range(3)]。

    它的缺点也很明显,就是所有数据都在内存里面,如果有海量的数据,将会非常耗内存。

      (2)生成器是可以迭代的,但是只可以读取它一次。因为用的时候才生成,比如a = (x*x for x in range(3))。!!!!注意这里是小括号而不是方括号。

      (3)生成器(generator)能够迭代的关键是他有next()方法,工作原理就是通过重复调用next()方法,直到捕获一个异常。

      (4)带有yield的函数不再是一个普通的函数,而是一个生成器generator,可用于迭代

      (5)yield是一个类似return 的关键字,迭代一次遇到yield的时候就返回yield后面或者右面的值。而且下一次迭代的时候,从上一次迭代遇到的yield后面的代码开始执行

      (6)yield就是return返回的一个值,并且记住这个返回的位置。下一次迭代就从这个位置开始。

      (7)带有yield的函数不仅仅是只用于for循环,而且可用于某个函数的参数,只要这个函数的参数也允许迭代参数。

      (8)send()和next()的区别就在于send可传递参数给yield表达式,这时候传递的参数就会作为yield表达式的值,而yield的参数是返回给调用者的值,也就是说send可以强行修改上一个yield表达式值。

      (9)send()和next()都有返回值,他们的返回值是当前迭代遇到的yield的时候,yield后面表达式的值,其实就是当前迭代yield后面的参数。

      (10)第一次调用时候必须先next()或send(),否则会报错,send后之所以为None是因为这时候没有上一个yield,所以也可以认为next()等同于send(None)

  • 相关阅读:
    字节面试:谈谈索引为什么能提高查询性能?
    阿里云服务器(ECS)上搭建rocketmq
    如何在 CentOS 8 上安装 Apache Maven
    147 01 Android 零基础入门 02 Java面向对象 07 Java多态 05 接口(重点)05 关于多接口中重名默认方法处理的解决方案
    146 01 Android 零基础入门 02 Java面向对象 07 Java多态 05 接口(重点)04 接口成员--默认方法 & 静态方法
    145 01 Android 零基础入门 02 Java面向对象 07 Java多态 05 接口(重点)03 接口成员--抽象方法&常量
    tcp循环发消息
    tcp基本语法
    界面开发控件DevExpress WPF开发指南
    高性能H5/JS开发框架DevExtreme 2021新版首发
  • 原文地址:https://www.cnblogs.com/sunBinary/p/10940974.html
Copyright © 2020-2023  润新知