Python中的迭代器与生成器高级用法

迭代器

迭代器是依附于迭代协议的对象——基本意味它有一个next方法(method),当调用时,返回序列中的下一个项目。当无项目可返回时,引发(raise)StopIteration异常。

迭代对象允许一次循环。它保留单次迭代的状态(位置),或从另一个角度讲,每次循环序列都需要一个迭代对象。这意味我们可以同时迭代同一个序列不只一次。将迭代逻辑和序列分离使我们有更多的迭代方式。

调用一个容器(container)的__iter__方法创建迭代对象是掌握迭代器最直接的方式。iter函数为我们节约一些按键。

>>> nums = [1,2,3]   # note that ... varies: these are different objects>>> iter(nums)              >>> nums.__iter__()           >>> nums.__reversed__()         >>> it = iter(nums)>>> next(it)      # next(obj) simply calls obj.next()1>>> it.next()2>>> next(it)3>>> next(it)Traceback (most recent call last): File "", line 1, in StopIteration

登录后复制

立即学习“Python免费学习笔记(深入)”;

当在循环中使用时,StopIteration被接受并停止循环。但通过显式引发(invocation),我们看到一旦迭代器元素被耗尽,存取它将引发异常。

使用for…in循环也使用__iter__方法。这允许我们透明地开始对一个序列迭代。但是如果我们已经有一个迭代器,我们想在for循环中能同样地使用它们。为了实现这点,迭代器除了next还有一个方法__iter__来返回迭代器自身(self)。

Python中对迭代器的支持无处不在:标准库中的所有序列和无序容器都支持。这个概念也被拓展到其它东西:例如file对象支持行的迭代。

>>> f = open('/etc/fstab')>>> f is f.__iter__()True

登录后复制

立即学习“Python免费学习笔记(深入)”;

file自身就是迭代器,它的__iter__方法并不创建一个单独的对象:仅仅单线程的顺序读取被允许。

生成表达式
第二种创建迭代对象的方式是通过 生成表达式(generator expression) ,列表推导(list comprehension)的基础。为了增加清晰度,生成表达式总是封装在括号或表达式中。如果使用圆括号,则创建了一个生成迭代器(generator iterator)。如果是方括号,这一过程被‘短路’我们获得一个列表list。

>>> (i for i in nums)           at 0x...>>>> [i for i in nums][1, 2, 3]>>> list(i for i in nums)[1, 2, 3]

登录后复制

立即学习“Python免费学习笔记(深入)”;

在Python 2.7和 3.x中列表表达式语法被扩展到 字典和集合表达式。一个集合set当生成表达式是被大括号封装时被创建。一个字典dict在表达式包含key:value形式的键值对时被创建:

>>> {i for i in range(3)}  set([0, 1, 2])>>> {i:i**2 for i in range(3)}  {0: 0, 1: 1, 2: 4}

登录后复制

立即学习“Python免费学习笔记(深入)”;

如果您不幸身陷古老的Python版本中,这个语法有点糟:

>>> set(i for i in 'abc')set(['a', 'c', 'b'])>>> dict((i, ord(i)) for i in 'abc'){'a': 97, 'c': 99, 'b': 98}

登录后复制

立即学习“Python免费学习笔记(深入)”;

生成表达式相当简单,不用多说。只有一个陷阱值得提及:在版本小于3的Python中索引变量(i)会泄漏。

生成器

生成器是产生一列结果而不是单一值的函数。

第三种创建迭代对象的方式是调用生成器函数。一个 生成器(generator) 是包含关键字yield的函数。值得注意,仅仅是这个关键字的出现完全改变了函数的本质:yield语句不必引发(invoke),甚至不必可接触。但让函数变成了生成器。当一个函数被调用时,其中的指令被执行。而当一个生成器被调用时,执行在其中第一条指令之前停止。生成器的调用创建依附于迭代协议的生成器对象。就像常规函数一样,允许并发和递归调用。
当next被调用时,函数执行到第一个yield。每次遇到yield语句获得一个作为next返回的值,在yield语句执行后,函数的执行又被停止。

>>> def f():...  yield 1...  yield 2>>> f()                  >>> gen = f()>>> gen.next()1>>> gen.next()2>>> gen.next()Traceback (most recent call last): File "", line 1, in StopIteration

登录后复制

立即学习“Python免费学习笔记(深入)”;

让我们遍历单个生成器函数调用的整个历程。

>>> def f():...  print("-- start --")...  yield 3...  print("-- middle --")...  yield 4...  print("-- finished --")>>> gen = f()>>> next(gen)-- start --3>>> next(gen)-- middle --4>>> next(gen)              -- finished --Traceback (most recent call last): ...StopIteration

登录后复制

立即学习“Python免费学习笔记(深入)”;

相比常规函数中执行f()立即让print执行,gen不执行任何函数体中语句就被赋值。只有当gen.next()被next调用,直到第一个yield部分的语句才被执行。第二个语句打印– middle –并在遇到第二个yield时停止执行。第三个next打印– finished –并且到函数末尾,因为没有yield,引发了异常。

当函数yield之后控制返回给调用者后发生了什么?每个生成器的状态被存储在生成器对象中。从这点看生成器函数,好像它是运行在单独的线程,但这仅仅是假象:执行是严格单线程的,但解释器保留和存储在下一个值请求之间的状态。

为何生成器有用?正如关于迭代器这部分强调的,生成器函数只是创建迭代对象的又一种方式。一切能被yield语句完成的东西也能被next方法完成。然而,使用函数让解释器魔力般地创建迭代器有优势。一个函数可以比需要next和__iter__方法的类定义短很多。更重要的是,相比不得不对迭代对象在连续next调用之间传递的实例(instance)属性来说,生成器的作者能更简单的理解局限在局部变量中的语句。

还有问题是为何迭代器有用?当一个迭代器用来驱动循环,循环变得简单。迭代器代码初始化状态,决定是否循环结束,并且找到下一个被提取到不同地方的值。这凸显了循环体——最值得关注的部分。除此之外,可以在其它地方重用迭代器代码。

双向通信
每个yield语句将一个值传递给调用者。这就是为何PEP 255引入生成器(在Python2.2中实现)。但是相反方向的通信也很有用。一个明显的方式是一些外部(extern)语句,或者全局变量或共享可变对象。通过将先前无聊的yield语句变成表达式,直接通信因PEP 342成为现实(在2.5中实现)。当生成器在yield语句之后恢复执行时,调用者可以对生成器对象调用一个方法,或者传递一个值 给 生成器,然后通过yield语句返回,或者通过一个不同的方法向生成器注入异常。

第一个新方法是send(value),类似于next(),但是将value传递进作为yield表达式值的生成器中。事实上,g.next()和g.send(None)是等效的。

第二个新方法是throw(type, value=None, traceback=None),等效于在yield语句处

raise type, value, traceback

登录后复制

立即学习“Python免费学习笔记(深入)”;

不像raise(从执行点立即引发异常),throw()首先恢复生成器,然后仅仅引发异常。选用单次throw就是因为它意味着把异常放到其它位置,并且在其它语言中与异常有关。

当生成器中的异常被引发时发生什么?它可以或者显式引发,当执行某些语句时可以通过throw()方法注入到yield语句中。任一情况中,异常都以标准方式传播:它可以被except和finally捕获,或者造成生成器的中止并传递给调用者。

因完整性缘故,值得提及生成器迭代器也有close()方法,该方法被用来让本可以提供更多值的生成器立即中止。它用生成器的__del__方法销毁保留生成器状态的对象。

让我们定义一个只打印出通过send和throw方法所传递东西的生成器。

>>> import itertools>>> def g():...   print '--start--'...   for i in itertools.count():...     print '--yielding %i--' % i...     try:...       ans = yield i...     except GeneratorExit:...       print '--closing--'...       raise...     except Exception as e:...       print '--yield raised %r--' % e...     else:...       print '--yield returned %s--' % ans>>> it = g()>>> next(it)--start----yielding 0--0>>> it.send(11)--yield returned 11----yielding 1--1>>> it.throw(IndexError)--yield raised IndexError()----yielding 2--2>>> it.close()--closing--

登录后复制

立即学习“Python免费学习笔记(深入)”;

注意: next还是__next__?

在Python 2.x中,接受下一个值的迭代器方法是next,它通过全局函数next显式调用,意即它应该调用__next__。就像全局函数iter调用__iter__。这种不一致在Python 3.x中被修复,it.next变成了it.__next__。对于其它生成器方法——send和throw情况更加复杂,因为它们不被解释器隐式调用。然而,有建议语法扩展让continue带一个将被传递给循环迭代器中send的参数。如果这个扩展被接受,可能gen.send会变成gen.__send__。最后一个生成器方法close显然被不正确的命名了,因为它已经被隐式调用。

链式生成器
注意: 这是PEP 380的预览(还未被实现,但已经被Python3.3接受)

比如说我们正写一个生成器,我们想要yield一个第二个生成器——一个子生成器(subgenerator)——生成的数。如果仅考虑产生(yield)的值,通过循环可以不费力的完成:

subgen = some_other_generator()for v in subgen:  yield v

登录后复制

立即学习“Python免费学习笔记(深入)”;

然而,如果子生成器需要调用send()、throw()和close()和调用者适当交互的情况下,事情就复杂了。yield语句不得不通过类似于前一章节部分定义的try…except…finally结构来保证“调试”生成器函数。这种代码在PEP 380中提供,现在足够拿出将在Python 3.3中引入的新语法了:

yield from some_other_generator()

登录后复制

立即学习“Python免费学习笔记(深入)”;

像上面的显式循环调用一样,重复从some_other_generator中产生值直到没有值可以产生,但是仍然向子生成器转发send、throw和close。

更多Python中的迭代器与生成器高级用法相关文章请关注PHP中文网!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至253000106@qq.com举报,一经查实,本站将立刻删除。

发布者:PHP中文网,转转请注明出处:https://www.chuangxiangniao.com/p/2278456.html

(0)
上一篇 2025年2月27日 16:47:37
下一篇 2025年2月27日 16:47:53

AD推荐 黄金广告位招租... 更多推荐

相关推荐

  • python正则表达教程2024

    正则表达式是一种在文本中匹配模式的代码,使用元字符和量词定义匹配模式。Python 中使用 re 模块处理正则表达式:1. 导入 re 模块;2. 定义正则表达式模式;3. 调用 re.search() 查找匹配项。正则表达式语法包括元字符…

    2025年3月30日
    100
  • java中字符串怎么转数组

    在 Java 中,将字符串转换为数组的方法有:使用 split() 方法根据模式分隔字符串;使用 toCharArray() 方法获取单个字符的字符数组;使用 replaceAll() 和 StringJoiner 替换空格并连接非空格字符…

    2025年3月30日
    100
  • java怎么由字符串构造数组

    在 Java 中,可以通过以下六种方法将字符串构造为数组:使用 String.split() 方法拆分字符串使用正则表达式拆分字符串使用字符数组和 Arrays.copyOfRange() 方法使用流将字符串拆分为字符流使用 Apache …

    2025年3月30日
    100
  • java怎么将字符串给数组

    Java中将字符串转换为数组的方法有5种:使用toCharArray()方法、正则表达式、split()方法、Arrays.copyOf()方法和Stream API。具体选择取决于具体需求,不同方法有各自的优点和缺点。 如何在 Java …

    2025年3月30日
    100
  • notepad++版本介绍

    Notepad++ 是一款开源文本编辑器,自 2003 年起不断更新。目前最新版本是 10.0.0,具有语法高亮、多视图编辑、宏录制、正则表达式搜索、Markdown 预览、语法检查和代码重构等广泛功能。 Notepad++ 版本介绍 No…

    2025年3月30日
    100
  • notepad++的作用是什么

    Notepad++ 是一款免费的文本编辑器,主要用于编程、网页开发和数据分析等文本处理任务。其作用包括:语法高亮,识别不同语言代码结构和错误;代码自动完成,提高编码效率和准确性;支持自定义宏和插件,满足特定需求;多文档界面,实现多任务处理;…

    2025年3月30日
    100
  • 使用正则表达式匹配合法的 IPv4 地址的操作方法

    使用正则表达式匹配合法 IPv4 地址的方法:使用正则表达式 ^(([0-9]|1-9|1[0-9]{2}|20-4|25[0-5]).){3}([0-9]|1-9|1[0-9]{2}|20-4|25[0-5])$,匹配四段数字,每段 0-…

    2025年3月30日
    100
  • vscode是干嘛的 vscode的作用

    VS Code:不仅是代码编辑器,更是编程伙伴VS Code 是一款功能强大的集成开发环境(IDE),提供丰富的扩展和工具,显著提升编码效率:高度可扩展性:拥有庞大的扩展市场,覆盖几乎所有编程语言、框架和工具。核心功能:强大的内置调试器,支…

    2025年3月30日
    100
  • 如何将复杂的LaTeX公式转换成Python或JavaScript代码进行数值计算?

    LaTeX公式到编程语言代码转换:挑战与解决方案 将LaTeX数学公式转换为Python或JavaScript等编程语言代码以进行数值计算,并非易事。LaTeX注重公式的排版美观,而编程语言则强调代码的执行逻辑。两者表达方式的差异,导致直接…

    2025年3月30日
    100
  • vscode使用deepseek

    VS Code 与 DeepSeek:高效代码搜索利器 vs code 已经成为许多开发者的首选代码编辑器,其强大的扩展生态系统更是锦上添花。deepseek 正是其中一款值得关注的扩展,它极大地提升了代码搜索的效率和准确性。 本文将深入探…

    编程技术 2025年3月30日
    100

发表回复

登录后才能评论