乐趣区

一篇夯实一个知识点系列--python生成器

写在后面

本系列目标:一篇文章,不求鞭辟入里,但使得心应手。

  • 迭代是数据处理的基石,在扫描内存无奈装载的数据集时,咱们须要一种惰性获取数据的能力(即一次获取一部分数据到内存)。在 Python 中,具备这种能力的对象就是迭代器。生成器是迭代器的一种非凡表现形式。

    • 集体认为生成器是 Python 中最有用的高级个性之一(甚至没有之一)。尽管高级编码中应用寥寥,但随着学习深刻,会发现生成器是协程,异步等高级常识的基石。Python 最有野心的 asyncio 库,就是用协程砌造的。

      注:生成器和协程实质雷同。PEP342(Python 加强提案)减少了生成器的 send()办法,使其变身为协程。如此之后,生成器生成数据,协程生产数据。尽管实质雷同,然而因为从理念上说协程跟迭代没有关系,并且纠缠生成器和协程的区别与分割会引爆本人的大脑,所以应该将这两个概念辨别。此处说实质雷同意为:了解生成器原理之后,了解减少了 send 办法,然而实现形式简直雷同的协程会更加轻松(这段话看不懂没有关系,船到桥头天然直,学到协程天然懂)。

  • Python 的一致性是其最迷人的中央。理解了 Python 生成器,迭代器的实现。就会对 Python 的一致性设计有更加强烈的感知。本文读完之后,遇到面试官发问为什么列表能够迭代,字典能够迭代,甚至文本文件都能够迭代时,你就能够稳 (huang) 得一批。
  • 浏览本文之前,如果你对 Python 的一致性有一些理解,如鸭子类型,或者 Cpython 的 PyObject 构造体,那真是太棒了。不过鉴于笔者深厚的文字功底,没有这些常识也不打紧。

干货儿

  • 迭代器

    在学习生成器之前,先要理解迭代器。顾名思义,迭代器即具备迭代性能的对象。在 Python 中,能够认为迭代器能够通过一直迭代,产生出一个又一个的对象。

    • 可迭代对象和迭代器

      Python 的一致性是靠协定撑持的。一个对象只有遵循以下协定,它就是一个可迭代对象或迭代器。

      • Python 中的一个对象,如果实现了 iter 办法,并且 iter 办法返回一个迭代器,那么它就是可迭代对象。如果实现了 iter 和 next 办法,并且 iter 办法返回一个迭代器,那么它就是迭代器(有点绕,按住不表,持续学习)。

        注:如果对象实现了__getitem__办法,并且索引从 0 开始,那么也是可迭代对象。此 hack 为兼容性思考。只需切记,如果你要实现可迭代对象和可迭代器,那么请遵循以上协定。

      • 可迭代对象的 iter 返回迭代器,迭代器的 iter 办法返回本身(也是迭代器),迭代器的 next 办法实现迭代性能,一直返回下一个元素,或者在元素为空时 raise 一个 StopIteration 终止迭代。
    • 可迭代对象与迭代器的关系

      话不多说,上代码。

      class Iterable:
          def __init__(self, *args):
              self.items = args
      
          def __iter__(self):
              return Iterator(self.items)       
      
      class Iterator:
          def __init__(self, items):
              self.items = items
              self.index = 0
      
          def __iter__(self):
              return self                       
      
          def __next__(self):                
              try:
                  item = self.items[self.index]
              except IndexError:
                  raise StopIteration()
              self.index += 1
              return item
      
      ins = Iterable(1,2,3,4,5)        # 1
      for i in ins:
          print(i)
      print('the end...')
      >>>                                              # 2
      1
      2
      3
      4
      5
      the end ...
      • 上述代码中,实现了可迭代对象 Iterable 和迭代器 Iterator。遵循协定规定,Iterable 实现了 iter 办法,且 iter 办法返回迭代器 Iterator 实例,迭代器实现了 iter 办法和 next 办法,iter 返回本身(即 sel,迭代器自身 f),next 办法返回迭代器中的元素或者引发 StopIteration 异样。运行上述代码,会看到 #2 处的输入。
      • 通过上述代码迭代一个对象显得非常啰嗦。比方在 Iterable 中,iter 必须要返回一个迭代器。为什么不能间接用 Iterator 迭代元素呢?假如咱们通过迭代器来迭代元素,将上述代码中的 #1 处如下代码:

        ins = Iterator([1,2,3,4,5])
        for i in ins:                                                        # 3
            print(i)
        for i in ins:                                                        # 4
            print(i)
        next(ins)                                                            # 5
        print('the end...')
        >>>                                                                      # 6
        1
        2
        3
        4
        5
        ...
        File "/home/disk/test/a.py", line 20, in __next__        # 7
            raise StopIteration()
        the end...

        运行上述代码,会看到 #6 处的输入。纳闷的是,#3 和#4 处运行了两次 for 循环,后果只打印一遍所有元素。解释如下:

        • 上述代码中,ins 是一个 Iterator 迭代器对象。那么 ins 合乎迭代器协定:每次调用 next,会返回下一个元素,直到迭代器元素为空,raise 一个 StopIteration 异样。
        • #3 处第一次通过 for 循环迭代 ins,相当于一直调用 ins 的 next 办法,一直返回下一个元素,输入如 #6 所示。当元素为空时,迭代器 raise 了 StopIterator。而这个异样会被 for 循环捕捉,不会裸露给用户,所以咱们就认为数据迭代实现,并且没有出现异常。
        • 迭代器 ins 内的元素曾经被 #3 处的 for 循环耗费完,并且 raise 了 StopIteration(只不过被 for 循环捕捉静默解决,没有裸露给用户)。此时 ins 曾经是元素耗费殆尽的“空”状态。在#4 处第二次通过 for 循环迭代 ins,因为 ins 内的元素为空,持续调用 ins 的 next 办法,那么还是会 raise 一个 StopIteration,而且又被 for 循环静默解决,所以没有异样,也没有输入。
        • 接下来,#5 处通过 next 办法获取 ins 的下一个元素,同上,持续 raise 一个 StopIteration 异样。因为此处通过 next 调用而不是 for 循环,异样不会被解决,所以抛出到用户层面,即 #7 输入。
        • 从新编写上述代码中 #3 处 for 循环和#4 处 for 循环,能够看到对应输入验证了咱们的论断。第一次 for 循环在迭代到元素为 2 时跳出循环,第二次 for 循环持续迭代同一个迭代器,那么会持续上次迭代器完结地位持续迭代元素。代码如下:

          ins = Iterator([1,2,3,4,5])
          print('the first for:')
          for i in ins:                                    # 3  the first for
            print(i)
            if i == 2:
                break
          print('the second for:')
          for i in ins:                                   # 4 the second for
                print(i)
          print('the end...')
          >>>                                                # the output
          the first for:
          1
          2
          the second for:
          3
          4
          5
          the end...

          所以咱们能够失去如下论断:

          • 一个迭代器对象只能迭代一遍。屡次迭代,相当于不停对一个空迭代器调用 next 办法,会不停 raise StopIteration 异样。
          • 因为迭代器实现了 iter 办法,并且 iter 办法返回了迭代器,那么迭代器也是一个可迭代对象(废话,不是可迭代对象,上述代码中如何能够用 for 循环迭代呢)
          • 综上来说,可迭代对象和迭代器显著是一个多态的问题。迭代器是一个可迭代对象,能够迭代返回元素,因为 iter 返回 self(即本身实例),所以只能迭代一遍,迭代到开端就会抛出异样。而每次迭代可迭代对象,iter 都会返回一个新的迭代器实例。所以可迭代对象是反对屡次迭代的。比方 l =[i for i in range(10)]生成的 list 对象就是一个可迭代对象,能够被屡次迭代。l=(i for i in range(10))生成的是一个迭代器,只能被迭代一遍。
    • 迭代器反对

      援用晦涩的 Python 中的原话,迭代器反对以下 6 个性能。因为篇幅所限,点到为止。大家只有了解了迭代器的原理,了解以下性能天然是瓜熟蒂落。

      • for 循环

        上述代码曾经有举例,可参考

      • 构建和扩大汇合类型

        from collections improt abc
        
        class NewIterator(abc.Iterator):
            pass                                                    # 放飞自我,实现新的类型
      • 列表推导,字典推导和汇合推导

        l = [i for i in range(10)]            # list
        d = {i:i for i in range(10)}      # dict
        s = {i for i in range(10)}           # set
      • 遍历文本文件

        with open ('a.txt') as f:
            for line in f:
                print(line)
      • 元祖拆包

        for i, j in [(1, 2), (3, 4)]:
            print(i,  j)
        >>>
        1 2
        3 4
      • 调用函数时,应用 * 拆包实参

        def func(a, b, c):
            print(a, b, c)
        
        func(*[1, 2, 3])  # 会将 [1, 2, 3] 这个 list 拆开成三个实参,对应 a, b, c 三个形参传给 func 函数
  • 生成器

    Python 之禅已经说过,simple is better than complex。鉴于以上代码中迭代器简单的实现形式。Python 提供了一个更加 pythonic 的实现形式——生成器。生成器函数就是含有 yield 关键字的函数(目前这种说法是正确的,之后会学到 yield from 等句法,那么这个说法就就须要更正了),生成器对象就是调用生成器函数返回的对象。

    • 生成器的实现

      将上述代码批改为生成器实现,如下:

      class Iterable:
          def __init__(self, *args):
              self.items = args
      
          def __iter__(self):                            # 8
              for item in self.items:
                  yield item
      
      ins = Iterable(1, 2, 3, 4, 5)
      print('the first for')
      for i in ins:
          print(i)
      print('the second for')
      for i in ins:
          print(i)
      print('the end...')
      
      >>>                                                            # 9                            
      the first for
      1
      2
      3
      4
      5
      the second for
      1
      2
      3
      4
      5
      the end...

      上述代码中,可迭代对象的 iter 办法并没有只用了短短数行,就实现了之前 Iterator 迭代器性能,点赞!

    • yield 关键字

      要了解以上代码,就须要了解 yield 关键字,先来看以下最简略的生成器函数实现

      def func():
          yield 1                                                                
          yield 2
          yield 3
      
      ins1 = func()
      ins2 = func()
      print(func)
      print(ins1)
      print(ins2)
      
      for i in ins1:
          print(i)
      for i in ins1:
          print(i)
      
      print(next(ins2))
      print(next(ins2))
      print(next(ins2))
      print(next(ins2))
      
      >>> 
      <function func at 0x7fcb1e4bde18>
      <generator object func at 0x7fcb1cc7c0a0>
      <generator object func at 0x7fcb1cc7c0f8>
      1
      2
      3
      1
      2
      3
        File "/home/disk/test/a.py", line 18, in <module>
          print(next(ins2))
      StopIteration

      从以上代码能够看出:

      • func 是一个函数,然而调用 func 会返回一个生成器对象,并且通过打印的地址看,每次调用生成器函数会返回一个新的生成器对象。
      • 生成器对象和迭代器对象类似,都能够被 for 循环迭代,都只能被迭代一遍,通过 next 调用,都会在生成器元素为空时 raise 一个 StopIteration 异样。
    那么含有 yield 关键字的生成器函数体是如何执行的呢?请看如下代码:```python
    def f_gen():                            # 10
        print('start')
        yield 1                                    # 11
        print('stop')
        yield 2                                    # 12
        print('next')
        yield 3                                    # 13
        print('end')
    
    for i in f_gen():                    # 14
        print(i)
    
    >>>
    start
    1
    stop
    2
    next
    3
    end
    ```
    从上述代码及其打印后果,咱们能够得出如下论断:-   \#10 处代码表明,生成器函数定义与一般函数无二,只是须要蕴含有 yield 关键字
    -   \#14for 循环隐形调用 next 的时候,会执行到 #11 处,打印 start,而后产出值 1 返回给 for 循环,打印
    -   for 循环持续调用 next,** 从 #11 处执行到#12 处 **#,打印 stop,而后产出值 2 返回给 for 循环,打印
    -   for 循环持续调用 next,** 从 #12 处执行到#13 处 **#,打印 next,而后产出值 3 返回给 for 循环,打印
    -   for 循环持续调用 next,** 从 #13 处执行到函数尾 **#,打印 end,而后 raise 一个 StopIteration,因为 for 循环捕捉异样,程序失常执行
    -   ** 综上所述,yield 具备暂停的性能,每次迭代生成器,生成器函数体都会后退到 yield 语句处,并将 yield 之后的值抛出(无值抛 None)。生成器函数作为一个工厂函数,实现了可迭代对象中 iter 函数的性能,能够每次产出一个新的迭代器实例。因为应用了非凡的 yield 关键字,它领有与区别于迭代器的新名字——生成器,它其实与迭代器并无二致 **
  • 生成器表达式

    将列表推导式中的 [] 改为(),即为生成器表达式。返回的是一个生成器对象。个别用户列表推导然而又不须要立马产生所有值的情景中。

    gen = (i for i in range(10))
    
    for i in gen:
        print(i)
    
    for i in gen:                        # 只能被生产一遍,第二遍无输入
        print(i)
    print('the end...')
    
    >>> 
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    the end...
  • itertools

    python 的内置模块 itertools 提供了对生成器的诸多反对。这里列举一个,其它反对请看文档

    gen = itertools.count(1, 2)    # 从 1 开始,步长为 2,一直产生数值
    
    >>> next(gen)
    1
    >>> next(gen)
    3
    >>> next(gen)
    5
    >>> next(gen)
    7
    >>> next(gen)
    9
    >>> next(gen)
    11
  • yield from 关键字

    yield from 是 python3.3 中呈现的新句法。yield from 句法能够实现委派生成器。

    def func():
        yield from (i for i in range(5))
    
    gen = func()
    
    for i in gen:
        print(i)
        
    >>>
    0
    1
    2
    3
    4

    如上所示,yield from 把 func 作为了一个委派生成器。for 循环能够通过委派生成器 func 间接迭代子生成器(i for i in range(5))。不过只是这个取巧远远不足以将 yield from 作为一个新句法退出到 Python 中。比起上述代码的迭代内层循环,新句法更加重要的性能是委派生成器为调用者和子生成器建设了一个管道。通过生成器的 send 办法就能够在管道中为两端传递音讯。如果应用此办法在程序层面控制线程行为,就会迸发出弱小的能量,它叫做协程。

写在最初


  • 注意事项

    迭代器与生成器功能强大,不过应用中还是有几点要留神:

    • 迭代器应该实现 iter 办法,尽管很多时候不实现此办法页不会影响代码运行。实现此办法的最次要起因有二:

      • 迭代器协定规定须要实现此办法
      • 能够通过 issubclass 查看对象是否是迭代器
    • 不要把可迭代对象变为迭代器。起因有二:

      • 这不合乎迭代器协定规定,造就了一个四不像。
      • 可迭代对象应该是能够反复遍历的,如果变为了迭代器,那么只能遍历一次。
  • tips

    集体感觉迭代器乏味的点

    • os.walk

      os.walk 迭代器能够深度遍历目录,是个大杀器,你值得领有,快去试试吧。

    • iter

      iter 能够承受两个地位参数:callable 和 flag。callable()能够一直产出值,如果等于 flag,则终止。如下是一个小例子

      gen = (i for i in range(10))
      for i in iter(lambda: next(gen), 4):                # 执行 ntext(gen),一直返回生成器中的值,等于 4 则进行
          print(i)
      
      >>> 
      0
      1
      2
      3
      the end...
    • yield 能够接管值

      yield 能够接管 send 发送的值。如下代码中,#16 处 send 的值,会传给 #15 中的 yield,而后赋值给 res。

      def func():
          res = yield 1                #15
          print(res)            
      
      f = func()
      f.send(None)              # 预激
      f.send(5)                        # 16

心愿大家能够通过本文把握装璜器这个杀手级个性。欢送关注集体博客: 药少敏的博客

退出移动版