美文网首页
python迭代对象,迭代器,生成器,以及yield用法详解(转

python迭代对象,迭代器,生成器,以及yield用法详解(转

作者: 裂开的汤圆 | 来源:发表于2019-05-28 06:17 被阅读0次

    原文章地址:http://python.jobbole.com/87805/

    学习这篇文章之前需要了解——迭代的概念

    对于迭代这个词,百度百科是这么翻译的——重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次迭代,每一次迭代得到的结果会作为下一次迭代初始值

    学习这篇文章之前需要了解——python assert断言语句,点我跳转

    让我们先看一张图,这张图解释了他们之间的关系

    这里写图片描述

    容器 (container)

    容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个地迭代获取,可以用in, not in关键字判断元素是否包含在容器中。通常这类数据结构把所有的元素存储在内存中(也有一些特例,并不是所有的元素都放在内存,比如迭代器和生成器对象)在Python中,常见的容器对象有:

    • list, deque, ….
    • set, frozensets, ….
    • dict, defaultdict, OrderedDict, Counter, ….
    • tuple, namedtuple, …
    • str

    容器比较容易理解,因为你就可以把它看作是一个盒子、一栋房子、一个柜子,里面可以塞任何东西。从技术角度来说,当它可以用来询问某个元素是否包含在其中时,那么这个对象就可以认为是一个容器,比如 list,set,tuples都是容器对象:

    这里写图片描述

    询问某元素是否在dict中用dict的中key:

    这里写图片描述

    询问某substring是否在string中:

    这里写图片描述

    尽管绝大多数容器都提供了某种方式来获取其中的每一个元素,但这并不是容器本身提供的能力,而是可迭代对象赋予了容器这种能力,当然并不是所有的容器都是可迭代的,比如:Bloom filter,虽然Bloom filter可以用来检测某个元素是否包含在容器中,但是并不能从容器中获取其中的每一个值,因为Bloom filter压根就没把元素存储在容器中,而是通过一个散列函数映射成一个值保存在数组中。

    可迭代对象(iterable)

    刚才说过,很多容器都是可迭代对象,此外还有更多的对象同样也是可迭代对象,比如处于打开状态的files,sockets等等。但凡是可以返回一个迭代器的对象都可称之为可迭代对象(可迭代对象可以通过iter()方法返回一个迭代器(iterator))。也可以简单的理解为可以直接作用于for循环的对象统称为可迭代对象(Iterable)。听起来可能有点困惑,没关系,先看一个例子:

    x = [1,2,3]
    
    y = iter(x)
    
    z = iter(x)
    
    next(y)
    Out[23]: 1
    
    next(y)
    Out[24]: 2
    
    type(x)
    Out[25]: list
    
    type(y)
    Out[26]: listiterator
    

    这里x是一个可迭代对象,可迭代对象和容器一样是一种通俗的叫法,并不是指某种具体的数据类型,list是可迭代对象,dict是可迭代对象,set也是可迭代对象。y和z是两个独立的迭代器,迭代器内部持有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。迭代器有一种具体的迭代器类型,比如list_iterator,set_iterator。可迭代对象实现了iter方法,该方法返回一个迭代器对象。
    当运行代码:

    x = [1,2,3]
    for i in x:
        ...
    

    实际执行情况是:


    这里写图片描述

    迭代器(iterator)

    那么什么迭代器呢?它是一个带状态的对象,他能在你调用next()方法的时候返回容器中的下一个值,任何实现了iternext()(python2中实现next())方法的对象都是迭代器,iter返回迭代器自身,next返回容器中的下一个值,如果容器中没有更多元素了,则抛出StopIteration异常。

    所以,迭代器就是实现了工厂模式的对象,它在你每次你询问要下一个值的时候给你返回。有很多关于迭代器的例子,比如itertools函数返回的都是迭代器对象。

    我们自定义一个迭代器

    # python2.7
    class test:
        def __init__(self):
            self.x = 0
    
        def __iter__(self):
            return self
    
        def next(self):
            value = self.x + 1
            self.x = value
    
            if self.x > 2:
                raise StopIteration
    
            return value
    
    t = test()
    a = iter(t)
    
    next(a)
    Out[47]: 1
    
    next(a)
    Out[67]: 2
    
    next(a)
    Traceback (most recent call last):
    
      File "<ipython-input-82-3f6e2eea332d>", line 1, in <module>
        next(a)
    
    StopIteration
    

    test既是一个可迭代对象(因为它实现了iter方法),又是一个迭代器(因为实现了next方法)。实例变量x维护迭代器内部的状态。每次调用next()方法的时候做两件事:

    1. 为下一次调用next()方法修改状态
    2. 为当前这次调用生成返回结果

    迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。

    生成器(generator)

    我们先来看一段官方对生成器的说明:

    Python’s generators provide a convenient way to implement the iterator protocol.
    

    意思大概是,python的生成器是一种以更优雅的方式去实现的迭代器,可见生成器是迭代器的一种。

    生成器算得上是Python语言中最吸引人的特性之一,生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要再像上面的类一样写iter()和next()方法了,只需要一个yiled关键字。 生成器一定是迭代器(反之不成立),因此任何生成器也是以一种懒加载的模式生成值。用生成器实现上面迭代器的例子是:

    def fib(): 
        x = 0 
        while x<2 : 
            x += 1 
            yield x
    

    运行结果:

    a = fib()
    
    next(a)
    Out[100]: 1
    
    next(a)
    Out[101]: 2
    
    next(a)
    Traceback (most recent call last):
    
      File "<ipython-input-102-3f6e2eea332d>", line 1, in <module>
        next(a)
    
    StopIteration
    
    type(a)
    Out[103]: generator
    

    fib就是一个普通的python函数,它特殊的地方在于函数体中没有return关键字,函数的返回值是一个生成器对象。当执行f=fib()返回的是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用next的时候才会真正执行里面的代码。

    yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

    生成器在Python中是一个非常强大的编程结构,可以用更少地中间变量写流式代码,此外,相比其它容器对象它更能节省内存和CPU,当然它可以用更少的代码来实现相似的功能。

    生成器表达式(generator expression)

    生成器表达式是列表推倒式的生成器版本,看起来像列表推导式,但是它返回的是一个生成器对象而不是列表对象。

    a = (x for x in range(10))
    
    type(a)
    Out[106]: generator
    
    next(a)
    Out[108]: 0
    

    列表推导式长什么样(将小括号换成中括号就是一个列表推导式了):

    b = [x for x in range(10)]
    
    b
    Out[110]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    深入学习yield(原文章地址)

    def g(): 
        print("1 is") 
        yield 1 
        print("2 is") 
        yield 2 
        print("3 is") 
        yield 3 
    
    >>>z = g() 
    >>>next(z) 
    1 is 
    1 
    >>>next(z) 
    2 is 
    2 
    >>>next(z) 
    3 is 
    3 
    >>>next(z) 
    Traceback (most recent call last):
    
      File "<ipython-input-166-7b32f85a2b4e>", line 1, in <module>
        next(z)
    
    StopIteration
    

    第一次调用next()方法时,函数似乎执行到yield 1,就暂停了。然后再次调用next()时,函数从yield 1之后开始执行的,并再次暂停。第三次调用next(),从第二次暂停的地方开始执行。第四次,抛出StopIteration异常。

    事实上,generator确实在遇到yield之后暂停了,确切点说,是先返回了yield表达式的值,再暂停的。当再次调用next()时,从先前暂停的地方开始执行,直到遇到下一个yield。这与上文介绍的对iterator调用next()方法,执行原理一般无二。

    有些教程里说generator保存的是算法,而我觉得用中断服务子程序来描述generator或许能更好理解,这样你就能将yield理解成一个中断服务子程序的断点,没错,是中断服务子程序的断点。我们每次对一个generator对象调用next()时,函数内部代码执行到”断点”yield,然后返回这一部分的结果,并保存上下文环境,”中断”返回。

    我们再来看另一段代码。

    def gen():
         while True:
             s = yield
             print(s)
    
    >>> g = gen()   
    
    >>> g.send('hello')     #这里很重要,向一个刚开始的生成器直接send一个值,会报错,所以我们得先调用next方法
                            #让生成器向后移动一个位置后再send值            
    Traceback (most recent call last):
    
      File "<ipython-input-169-9d017dfb1443>", line 1, in <module>
        g.send('hello')
    
    TypeError: can't send non-None value to a just-started generator
    
    # 下面才是send函数正确的使用方法
    >>> next(g)
    
    >>> g.send('hello')
    hello
    

    我也是看到这个形式的generator,懵了,才想要深入学习generator与yield的。结合以上的知识,我再告诉你,generator其实有第2种调用方法(恢复执行),即通过send(value)方法将value作为yield表达式的当前值,你可以用该值再对其他变量进行赋值,这一段代码就很好理解了。当我们调用send(value)方法时,generator正由于yield的缘故被暂停了。此时,send(value)方法传入的值作为yield表达式的值,函数中又将该值赋给了变量s,然后print函数打印s,循环再遇到yield,暂停返回。

    调用send(value)时要注意,要确保,generator是在yield处被暂停了,如此才能向yield表达式传值,否则将会报错(如上所示),可通过next()方法或send(None)使generator执行到yield。

    再来看一段yield更复杂的用法,或许能加深你对generator的next()与send(value)的理解。

    >>> def echo(value=None):
    ...   while 1:
    ...     value = (yield value)
    ...     print("The value is", value)
    ...     if value:
    ...       value += 1
    ...
    >>> g = echo(1)
    >>> next(g)
    1
    >>> g.send(2)
    The value is 2
    3
    >>> g.send(5)
    The value is 5
    6
    >>> next(g)
    The value is None
    

    上述代码既有yield value的形式,又有value = yield形式,看起来有点复杂。但以yield分离代码进行解读,就不太难了。第一次调用next()方法,执行到yield value表达式,保存上下文环境暂停返回1。第二次调用send(value)方法,从value = yield开始,打印,再次遇到yield value暂停返回。后续的调用send(value)或next()都不外如是。

    但是,这里就引出了另一个问题,yield作为一个暂停恢复的点,代码从yield处恢复,又在下一个yield处暂停。可见,在一次next()(非首次)或send(value)调用过程中,实际上存在2个yield,一个作为恢复点的yield与一个作为暂停点的yield。因此,也就有2个yield表达式。send(value)方法是将值传给恢复点yield;调用next()表达式的值时,其恢复点yield的值总是为None,而将暂停点的yield表达式的值返回。为方便记忆,你可以将此处的恢复点记作当前的(current),而将暂停点记作下一次的(next),这样就与next()方法匹配起来啦。

    小结

    1. 可迭代对象(Iterable)是实现了iter()方法的对象,通过调用iter()方法可以获得一个迭代器(Iterator)。
    2. 迭代器(Iterator)是实现了iter()和next()的对象。
    3. for … in …的迭代,实际是将可迭代对象转换成迭代器,再重复调用next()方法实现的。
    4. 生成器(generator)是一个特殊的迭代器,它的实现更简单优雅
    5. ieyield是生成器实现next()方法的关键。它作为生成器执行的暂停恢复点,可以对yield表达式进行赋值,也可以将yield表达式的值返回。

    相关文章

      网友评论

          本文标题:python迭代对象,迭代器,生成器,以及yield用法详解(转

          本文链接:https://www.haomeiwen.com/subject/jibltctx.html