我的编程空间,编程开发者的网络收藏夹
学习永远不晚

只要3分钟,Python生成器原理详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

只要3分钟,Python生成器原理详解


翻译:你逗比

segmentfault.com/a/1190000011330511


这篇文章是对 500 Lines or Less 一书中高效爬虫一章的部分翻译,原文:How Python Generators Work。建议结合《流畅的 Python》食用。


在掌握 Python 生成器之前,你必须了解常规 Python 函数的工作原理。通常,当一个 Python 函数调用子程序(subroutine)时,这个子程序将一直持有控制权,只有当子程序结束(返回或者抛出异常)后,控制权才还给调用者:


>>> def foo():

...     bar()

...

>>> def bar():

...     pass


标准的 Python 解释器是用 C 写的。解释器用一个叫做 PyEval_EvalFrameEx 的 C 函数来执行 Python 函数。它接受一个 Python 的堆栈帧(stack frame)对象,并在这个堆栈帧的上下文中执行 Python 字节码。这是 foo 的字节码:


>>> import dis

>>> dis.dis(foo)

  2           0 LOAD_GLOBAL              0 (bar)

              3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)

              6 POP_TOP

              7 LOAD_CONST               0 (None)

             10 RETURN_VALUE


foo 函数将 bar 加载到堆栈中并调用它,然后从堆栈中弹出返回值,最后加载并返回 None。


当 PyEval_EvalFrameEx 遇到 CALL_FUNCTION 字节码的时候,它会创建一个新的 Python 堆栈帧,然后用这个新的帧作为参数递归调用 PyEval_EvalFrameEx 来执行 bar。


Python 的堆栈帧是分配在堆内存中的,理解这一点非常重要!Python 解释器是个普通的 C 程序,所以它的堆栈帧就是普通的堆栈。但是它操作的 Python 堆栈帧是在堆上的。除了其他惊喜之外,这意味着 Python 的堆栈帧可以在它的调用之外存活。(FIXME: 可以在它调用结束后存活)。要以交互方式查看,请从 bar 内保存当前帧:


>>> import inspect

>>> frame = None

>>> def foo():

...     bar()

...

>>> def bar():

...     global frame

...     frame = inspect.currentframe()

...

>>> foo()

>>> # The frame was executing the code for 'bar'.

>>> frame.f_code.co_name

'bar'

>>> # Its back pointer refers to t

>>> def bar():

...     global frame

...     frame = inspect.currentframe()he frame for 'foo'.

>>> caller_frame = frame.f_back

>>> caller_frame.f_code.co_name

'foo'


aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3Bu

现在这项技术被用到了 Python 生成器(generator)上——使用代码对象和堆栈帧这些相同的组件来产生奇妙的效果。


这是一个生成器函数(generator function):


>>> def gen_fn():

...     result = yield 1

...     print('result of yield: {}'.format(result))

...     result2 = yield 2

...     print('result of 2nd yield: {}'.format(result2))

...     return 'done'

...


当 Python 将 gen_fn 编译为字节码时,它会看到 yield 语句,然后知道 gen_fn 是个生成器函数,而不是普通函数。它会设置一个标志来记住这个事实:


>>> # The generator flag is bit position 5.

>>> generator_bit = 1 << 5

>>> bool(gen_fn.__code__.co_flags & generator_bit)

True


当你调用一个生成器函数时,Python 会看到生成器标志,实际上并不运行该函数,而是创建一个生成器(generator):


>>> gen = gen_fn()

>>> type(gen)

<class 'generator'>


Python 生成器封装了一个堆栈帧和一个对生成器函数代码的引用,在这里就是对 gen_fn 函数体的引用:


>>> gen.gi_code.co_name

'gen_fn'


调用 gen_fn 产生的所有生成器都指向同一个代码对象,但是每个都有自己的堆栈帧。这个堆栈帧并不存在于实际的堆栈上,它在堆内存上等待着被使用


aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3Bu

堆栈帧有个 “last instruction”(FIXME: translate this or not?) 指针,指向最近执行的那条指令。刚开始的时候 last instruction 指针是 -1,意味着生成器尚未开始:


>>> gen.gi_frame.f_lasti

-1


当我们调用 send 时,生成器达到第一个 yield 处然后暂停执行。send 的返回值是 1,这是因为 gen 把 1 传给了 yield 表达式:


>>> gen.send(None)

1


现在生成器的指令指针(instruction pointer)向前移动了 3 个字节码,这些是编译好的 56 字节的 Python 代码的一部分:


>>> gen.gi_frame.f_lasti

3

>>> len(gen.gi_code.co_code)

56


生成器可以在任何时候被任何函数恢复执行,因为它的堆栈帧实际上不在堆栈上——它在堆(内存)上。生成器在调用调用层次结构中的位置不是固定的,它不需要遵循常规函数执行时遵循的先进后出顺序。生成器被是被解放了的,它像云一样浮动。


我们可以将 “hello” 发送到这个生成器中,它会成为 yield 表达式的值,然后生成器会继续执行,直到产出(yield)了 2:


>>> gen.send('hello')

result of yield: hello

2


现在这个生成器的堆栈帧包含局部变量 result:


>>> gen.gi_frame.f_locals

{'result': 'hello'}


从 gen_fn 创建的其他生成器将具有自己的堆栈帧和局部变量。


当我们再次调用 send 时,生成器将从它第二个 yield 处继续执行,然后以产生特殊异常 StopIteration 结束:


>>> gen.send('goodbye')

result of 2nd yield: goodbye

Traceback (most recent call last):

    File "<input>", line 1, in <module>

StopIteration: done


异常有一个值,它是那个生成器的返回值:字符串 “done”。


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

只要3分钟,Python生成器原理详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

只要3分钟,Python生成器原理详解

翻译:你逗比segmentfault.com/a/1190000011330511这篇文章是对 500 Lines or Less 一书中高效爬虫一章的部分翻译,原文:How Python Generators Work。建议结合《流畅的
2023-01-31

Python 3 之 生成器详解

------ 生成器 ------------------------------------------------------------------如今Python对延迟提供更多的支持——它提供了工具在需要的时候才产生结果,而不是立即
2023-01-31

MyBatisPlus代码生成器的原理及实现详解

这篇文章主要为大家详细介绍了MyBatisPlus中代码生成器的原理及实现,文中的示例代码讲解详细,对我们学习MyBatisPlus有一定帮助,需要的可以参考一下
2022-11-13

详解JS前端使用迭代器和生成器原理及示例

这篇文章主要为大家介绍了详解JS前端使用迭代器和生成器原理及示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-21

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录