写在前面

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

  • 迭代是数据处理的基石,在扫描内存无法装载的数据集时,我们需要一种惰性获取数据的能力(即一次获取一部分数据到内存)。在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关键字的生成器函数体是如何执行的呢?请看如下代码:

      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

希望大家可以通过本文掌握装饰器这个杀手级特性。欢迎关注个人博客:药少敏的博客

一篇夯实一个知识点系列--python生成的更多相关文章

  1. 一篇夯实一个知识点系列--python装饰器

    写在前面 本系列目的:希望可以通过一篇文章,不望鞭辟入里,但求在工程应用中得心应手. 装饰器模式是鼎鼎大名的23种设计模式之一.装饰器模式可以在不改变原有代码结构的情况下,扩展代码功能. Python ...

  2. 一篇夯实一个知识点系列--python实现十大排序算法

    写在前面 排序是查找是算法中最重要的两个概念,我们大多数情况下都在进行查找和排序.科学家们穷尽努力,想使得排序和查找能够更加快速.本篇文章用Python实现十大排序算法. 干货儿 排序算法从不同维度可 ...

  3. 每日一个知识点系列:volatile的可见性原理

    每日一个知识点系列的目的是针对某一个知识点进行概括性总结,可在一分钟内完成知识点的阅读理解,此处不涉及详细的原理性解读. img 看图说话 关键点1: 总线嗅探器(MESI 缓存一致性原理 ) 关键点 ...

  4. Python生成随机数的方法

    这篇文章主要介绍了Python生成随机数的方法,有需要的朋友可以参考一下 如果你对在Python生成随机数与random模块中最常用的几个函数的关系与不懂之处,下面的文章就是对Python生成随机数与 ...

  5. 每日一个知识点:Volatile 和 CAS 的弊端之总线风暴

    每日一个知识点系列的目的是针对某一个知识点进行概括性总结,可在一分钟内完成知识点的阅读理解,此处不涉及详细的原理性解读. 一.什么是总线风暴 总线风暴,听着真是一个帅气的词语,但如果发生在你的系统上那 ...

  6. 夯实Java基础系列1:Java面向对象三大特性(基础篇)

    本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 [https://github.com/h2pl/Java-Tutorial](https: ...

  7. 【转】C# 串口操作系列(1) -- 入门篇,一个标准的,简陋的串口例子。

    C# 串口操作系列(1) -- 入门篇,一个标准的,简陋的串口例子. 标签: c#objectnewlineexceptionbytestring 2010-05-17 01:10 117109人阅读 ...

  8. python excel操作 练习:#生成一个excel文件,生成3个sheet,每个sheet的a1写一下sheet的名称。每个sheet有个底色

    练习:#生成一个excel文件,生成3个sheet,每个sheet的a1写一下sheet的名称.每个sheet有个底色 #coding=utf-8 from openpyxl import Workb ...

  9. 夯实Java基础系列9:深入理解Class类和Object类

    目录 Java中Class类及用法 Class类原理 如何获得一个Class类对象 使用Class类的对象来生成目标类的实例 Object类 类构造器public Object(); register ...

随机推荐

  1. [每日一题2020.06.15]P1226 【模板】快速幂取余运算

    我是题目 快速幂就是快速求 \(a^b\)的一种算法 快速幂 思想 : 比如我要求 \(6^9\) 首先将幂转化为二进制形式 : \[6^9 = 6^{1001} \tag{1} \] 可以得到 : ...

  2. iOS开发实践-OOM治理

    概览 说起iOS的OOM问题大家第一想到的应该更多的是内存泄漏(Memory Leak),因为无论是从早期的MRC还是2011年Apple推出的ARC内存泄漏问题一直是iOS开发者比较重视的问题,比如 ...

  3. cb34a_c++_STL_算法_查找算法_(7)_lower_bound

    cb34a_c++_STL_算法_查找算法_(7)_lower_bound//针对已序区间的查找算法,如set,multiset关联容器-自动排序lower_bound()--第一个可能的位置uppe ...

  4. 【SEED Labs】DNS Rebinding Attack Lab

    Lab Overview 实验环境下载:https://seedsecuritylabs.org/Labs_16.04/Networking/DNS_Rebinding/ 在这个实验中模拟的物联网设备 ...

  5. Module Error (from ./node_modules/eslint-loader/index.js):解决办法

    vue启动项目报如下错误: Failed to compile. ./src/components/Vcontent.vue Module Error (from ./node_modules/esl ...

  6. 尚硅谷ajax视频教程2

    7.7. 尚硅谷_佟刚_Ajax_典型应用_验证用户名是否可用 整个项目的目录路径如下所示 我们首先新建立一个web工程,在webroot下面新建立一个script的文件夹,导入jquer文件 接下来 ...

  7. 【Vim命令】

    命令 操作 :set nu 显示行号  i 编辑模式  :wq  修改并退出  :%s/a/b  把所有的a替换成b                        

  8. SpringBoot--集成actuator

    actuator是spring boot项目中非常强大一个功能,有助于对应用程序进行监视和管理,通过 restful api 请求来监管.审计.收集应用的运行情况,针对微服务而言它是必不可少的一个环节 ...

  9. HDU 2157 How many ways?【矩阵快速幂】

    题目 春天到了, HDU校园里开满了花, 姹紫嫣红, 非常美丽. 葱头是个爱花的人, 看着校花校草竞相开放, 漫步校园, 心情也变得舒畅. 为了多看看这迷人的校园, 葱头决定, 每次上课都走不同的路线 ...

  10. 部署rabbitMQ镜像集群实战测试

    部署rabbitMQ镜像集群 版本信息 rabbit MQ: 3.8.5 Erlang: 官方建议最低21.3 推荐22.x 这里用的是23 环境准备 主机规划 主机 节点 172.16.14.3 磁 ...