[Python学习日记-42] Python 中的生成器

news2024/12/23 5:44:02

[Python学习日记-42] Python 中的生成器

简介

表达式生成器

函数生成器

用生成器实现并发编程

简介

        Python 中的生成器(Generator)是一种特殊的迭代器,它又被成为惰性运算,它可以在迭代过程中动态生成值,而不需要事先把所有的值存储在内存中。生成器可以通过两种方式来定义:使用生成器表达式或函数生成器。下面我们一起来看看生成器到底是怎么回事吧。

表达式生成器

        在前面学习了列表生成式,通过列表生成式,我们可以直接创建一个列表。但是会受到内存限制,所以列表容量肯定是有限的。如果创建一个包含100万个元素的列表,不仅会占用很大的存储空间,而且如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。例如要循环100万次,如果按 Python 的语法,代码如下

注意:Python3 对 range() 进行了优化,使用了生成器,所以我们要切换到 Python2 当中来查看

# Python2 的环境下
for i in range(1000000):
    print(i)

# 会看到包含0-999999的列表的出现
print(range(1000000))

代码输出如下:

        上面的代码会先生成100万个值的列表。假如现在循环到第50次时就不想继续就退出了。但是90多万的列表元素就白为你提前生成了。代码如下

# Python2 的环境下
for i in range(1000000):
    if i == 50:
        break
    print(i)

         所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?像上面代码中的循环,每次循环只是 +1 而已。我们完全可以写一个算法,让他执行一次就自动 +1,这样就不必创建完整的列表了,从而节省大量的空间。在 Python 中,这种一边循环一边计算后面元素的机制,称为生成器(generator)。
        要创建一个生成器,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [] 改成 (),就创建了一个生成器了,即生成器表达式,代码如下

# 列表生成式
l = [x * x for x in range(10)]
print(l)

# 生成器
l = (x * x for x in range(10))
print(l)

代码输出如下:

        上面的代码当中 (x * x for x in range(10)) 生成的就是一个生成器了。可问题来了,我们可以直接打印出列表当中的每一个元素,但是我们怎么打印出生成器的每一个元素呢?

        如果要一个一个打印出来,可以通过 next() 函数获得生成器的下一个返回值,代码如下

g = (x * x for x in range(10))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))

代码输出如下:

        前面我们讲过,生成器保存的是算法,每次调用 next(g) 就计算出 g 的下一个元素的值,直到计算到最后一个元素,当没有更多的元素时,则会抛出 StopIteration 的错误。当然,像上面代码那样不断地使用 next(g) 来获取下一个值实在是不太正常,正确的方法是使用 for 循环来获取,这是因为生成器也是一个可迭代(遍历)对象,代码如下

g = (x * x for x in range(10))

for i in g:
    print(i)

代码输出如下:

函数生成器

         生成器非常强大,但是如果推算的算法比较复杂,用生成器表达式生成的生成器配合 for 循环无法实现想要效果的时候,还可以用函数来实现生成器。例如著名的斐波拉契数列(Fibonacci),即除第一个和第二个数外,任意一个数都可由前两个数相加得到,如下

1,1,2,3,5,8,13,21,34,...

        下面先试用普通代码来实现100以内的斐波拉契数列,代码如下

a,b = 0,1
n = 0    # 斐波那契数
while n < 100:
    n = a + b
    a = b    # 把 b 的旧值给到 a
    b = n    # 新的 b = a + b(旧 b 的值)
    if n > 100:
        break
    else:
        print(n)

代码输出如下:

 

        下面我们把上面的代码改写成函数看看应该如何实现呢?代码如下

def fib(max):
    a,b = 0,1
    n = 0    # 斐波那契数
    while n < max:
        n = a + b
        a = b    # 把 b 的旧值给到 a
        b = n    # 新的 b = a + b(旧 b 的值)
        if n > 100:
            break
        else:
            print(n)

fib(100)

代码输出如下:

         从输出可以看出,fib() 函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似生成器。也就是说,上面的 fib() 函数和生成器仅一步之遥。要把 fib() 函数变成生成器,只需要把 print(n) 改为 yield n 就可以了,代码如下

def fib(max):
    a,b = 0,1
    n = 0    # 斐波那契数
    while n < max:
        n = a + b
        a = b    # 把 b 的旧值给到 a
        b = n    # 新的 b = a + b(旧 b 的值)
        if n > 100:
            break
        else:
            yield n     # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒

f = fib(100)    # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
print(next(f))
print(next(f))
print(f.__next__())     # next(f) 和 f.__next__() 是一样的
print(f.__next__())

代码输出如下:

        这就是定义生成器的另一种方法,函数生成器。如果一个函数定义中包含 yield 关键字,那么这个函数就不再是一个普通函数,而是一个函数生成器。

        这里,最难理解的就是生成器和函数的执行流程不一样。函数是顺序执行的,遇到 return 语句或者最后一行函数语句就返回。而变成生成器的函数后,在每次调用 next() 的时候函数才执行,遇到 yield 语句就暂停并返回数据到函数外,再次被 next() 调用时从上次返回的 yield 语句处继续执行。

        我们尝试一下在总多 next() 和 __next__() 中插入一些别的事情,看看会不会有影响,代码如下

def fib(max):
    a,b = 0,1
    n = 0    # 斐波那契数
    while n < max:
        n = a + b
        a = b    # 把 b 的旧值给到 a
        b = n    # 新的 b = a + b(旧 b 的值)
        if n > 100:
            break
        else:
            yield n     # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒

f = fib(100)    # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
print(next(f))
print(next(f))
print(f.__next__())     # next(f) 和 f.__next__() 是一样的
print("干点别的事情")
print(f.__next__())

代码输出如下:

        在上面输出中,我们在循环过程中不断调用 yield,函数就会不断的中断(暂停),即使中间有其他代码插进来运行了。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。同样的,把函数改成生成器后,我们基本上从来不会用 next() 来获取下一个返回值,而是直接
使用 for 循环来迭代,代码如下

def fib(max):
    a,b = 0,1
    n = 0    # 斐波那契数
    while n < max:
        n = a + b
        a = b    # 把 b 的旧值给到 a
        b = n    # 新的 b = a + b(旧 b 的值)
        if n > 100:
            break
        else:
            yield n     # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒

f = fib(100)    # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
for i in f:
    print(i)

代码输出如下:

用生成器实现并发编程

        虽然我们还没讲过并发编程,但我们肯定听过 CPU 多少核或者多少线程之类的,CPU 的多核就是为了可以实现并行运算的,就是让你同时边听歌、边聊 QQ、边刷知乎。而单核的 CPU 同一时间只能干一个事,所以你用单核电脑同时做好几件事的话,就会变的很慢,因为cpu要在不同程序任务间来回切换。
        而通过函数生成器(yield),我们可以实现单核下并发做多件事的效果,即单线程多并发。在此之前我们要先说一个知识点,上面说了 yield 可以返回运算后的结果后暂停函数,那我们能不能把数据传进去然后暂停函数呢?这当然是可以的,我们只需要把 yield n 后面的 n 去掉,然后把 yield 赋值给一个变量就好了,并且传入的方法从 next()/__next__() 变为 send(),代码如下

def g_test():
    while True:
        n = yield # 收到的值给n
        print("receive from outside:",n)

g = g_test()
g.__next__() # 调用生成器,同时会发送None到yield

for i in range(10):
    # can't send non-None value to a just-started generator
    # 生成器一开始需要发送一个None的值来激活yield,send()不能发送空值
    g.send(i) # 调用生成器,同时发送i

代码输出如下:

        上面的代码有几点需要注意的,yield 的输入要求一定要先运行 __next__() 或者 next(),然后再使用 send() 发送数据到函数,否则会抛出 TypeError 的错误,如下图所示

        这是因为在没有进行第一次 next()/__next__() 之前函数并没有运行,就是说这个时候函数并没有进行初始化,而当运行了第一次 next()/__next__() 之后函数则会在 yield 处暂停,这个时候运行 send() 就会畅通无阻了。

        说完这个知识点我们回到如何通过函数生成器(yield)实现单线程多并发的,我们以一个包子店的例子为例,我们的需求有以下几点:

  • 包子店会接待多个消费者
  • 包子店的师傅会在消费者吃包子的时候会生产一批包子
  • 师傅做包子的时候消费者也在吃包子

        代码如下

# 吃包子的消费者 c1,c2,c3

def consumer(name):
    print("消费者%s准备吃包子啦。。。"%name)
    while True:
        baozi = yield # 接受外面的包子
        print("消费者%s收到包子:%s"%(name,baozi))

c1 = consumer("C1")
c1.__next__()
c2 = consumer("C2")
c2.__next__()
c3 = consumer("C3")
c3.__next__()

for i in range(10):
    print("--------生产了第%s批包子--------"%i)
    c1.send(i)
    c2.send(i)
    c3.send(i)

代码输出如下:

        从输出来看包子店的师傅在一边制作包子,一边分发给每一位顾客,并不是像普通的函数那样,先做好所有包子,然后再分发给顾客。函数生成器(yield)是如何实现单线程多并发的呢?其实它并不是同时在工作的,在 CPU 看来都是顺序作业,只不过速度非常快,从人的感觉来看就像是在并行工作一样。

        他们的工作顺序是这样的:消费者 C1 到店——消费者 C2 到店——消费者 C3 到店——生产第1批包子——消费者 C1 收到包子:1——消费者 C2 收到包子:1——消费者 C3 收到包子:1——生产第2批包子——消费者 C1 收到包子:2——消费者 C2 收到包子:2——消费者 C3 收到包子:2——生产第3批包子...

        如此类推,这就是通过函数生成器(yield)实现单线程多并发了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2198241.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTML CSS 基础

HTML & CSS 基础 HTML一、HTML简介1、网页1.1 什么是网页1.2 什么是HTML1.3 网页的形成1.4总结 2、web标准2.1 为什么需要web标准2.2 Web 标准的构成 二、HTML 标签1、HTML 语法规范1.1基本语法概述1.2 标签关系 2、 HTML 基本结构标签2.1 第一个 HTML 网页2.2 基本结构标签…

PHP input 多文件上传功能实现-网页不为人知的数据库缺陷——未来之窗行业应用跨平台架构

一、多文件上传html部分 1.1错误示例 <input type"file" class"input fl" name"file" style"width:200px;display:inline;border:0px;" multiple />1.2 正确示例 <input type"file" class"input fl" …

Vxe UI vue vxe-table 实现表格单元格选中功能

Vxe UI vue vxe-table 实现表格单元格选中功能 在表格中实现鼠标点击任意单元格&#xff0c;选取的功能&#xff0c;通过 mouse-config 配置就可以开启单选功能&#xff0c;多选单元格选取功能需安装插件支持。 代码 参数说明 mouse-config 鼠标配置项&#xff1a; selected&…

Linux之shell详解(Linux Shell Detailed Explanation)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

【韩顺平Java笔记】第8章:面向对象编程(中级部分)【285-296】

文章目录 285. 为什么需要继承286. 继承原理图287. 继承快速入门288. 289. 290. 291. 292. 继承使用细节1,2,3,4,5288.1 继承给编程带来的便利288.2 继承的深入讨论/细节问题 293. 继承本质详解294. 继承课堂练习1295. 继承课堂练习2296. 继承课堂练习3 285. 为什么需要继承 28…

【软件部署安装】OpenOffice转换PDF字体乱码

现象与原因分析 执行fc-list查看系统字体 经分析发现&#xff0c;linux默认不带中文字体&#xff0c;因此打开我们本地的windows系统的TTF、TTC字体安装到centos机器上。 安装字体 将Windows的路径&#xff1a; C:\Windows\Fonts 的中文字体&#xff0c;如扩展名为 TTC 与TT…

App模拟心跳长连接的实现方法demo

摘要 背景&#xff1a;心跳通常是指客户端或服务器定期发送一个小型的、空的消息以保持连接的活动状态。它用于检测连接是否仍然有效&#xff0c;并防止连接由于长时间没有活动而被关闭。 技术原理&#xff1a;App定时发消息给服务器&#xff0c;服务器回消息表示连接依旧有效…

手机竖屏 Premiere Pro 电影转场特效视频模板Pr工程文件

10个不同的类别和115个过渡。过渡很容易使用。随附视频教程。 下载地址&#xff1a;Pr模板网 下载链接&#xff1a;https://prmuban.com/40597.html

动态规划算法专题(五):子序列问题

目录 1、最长递增子序列 1.1 算法原理 1.2 算法代码 2、摆动序列 2.1 算法原理 2.2 算法代码 3、最长递增子序列的个数 3.1 算法原理 3.2 算法代码 4、最长数对链 4.1 算法原理 4.2 算法代码 5、最长定差子序列 5.1 算法原理 5.2 算法代码 6、最长的斐波那契子序…

NASA:气象追踪分子光谱(ATMOS)二级产品,包含在垂直高度(公里)网格上的微量气体

目录 简介 摘要 引用 网址推荐 0代码在线构建地图应用 机器学习 ATMOS L2 Trace Gases on Altitude Grid, Tab Delimited Format V3 (ATMOSL2AT) at GES DISC 简介 这是版本3的气象追踪分子光谱&#xff08;ATMOS&#xff09;二级产品&#xff0c;包含在垂直高度&#…

多线程股吧(东方财富)用户信息爬取

多线程东方财富&#xff08;股吧&#xff09;用户信息爬取 在上一篇博客股吧信息爬取的基础上加入了多线程&#xff0c;使得速度提升了十几倍&#xff0c;爬取内容如下&#xff1a; 最终爬取结果如下&#xff1a; 完整代码如下&#xff08;准备好环境&#xff0c;比如pytho…

安宝特案例 | Fundació Puigvert 医院应用AR技术开创尿石症治疗新纪元

案例介绍 在医疗科技不断进步的今天&#xff0c;Fundaci Puigvert 医院迈出了重要一步&#xff0c;成功应用AR技术进行了全球首例同时使用两台内窥镜的ECIRS手术&#xff08;内镜肾内联合手术&#xff09;&#xff0c;由Esteban Emiliani M.D. PhD F.E.B.U 博士主刀。这标志着…

yub‘s Algorithmic Adventures_Day7

环形链表 link&#xff1a;https://leetcode.cn/problems/linked-list-cycle-ii/description/ 思路分析 我只能说双指针yyds【刻板hh】 我们分两种情况来分析 起码在第二圈才会相遇 fast比slow多走环的整数倍 fast 走的步数是 slow 步数的 2 倍&#xff0c;即 f2s&#xff…

5.资源《Arduino UNO R3 proteus 使用CD4511驱动数码管工程文件(含驱动代码)》说明。

资源链接&#xff1a; Arduino UNO R3 proteus 使用CD4511驱动数码管工程文件&#xff08;含驱动代码&#xff09; 1.文件明细&#xff1a; 2.文件内容说明 包含&#xff1a;proteus工程&#xff0c;内含设计图和工程代码。 3.内容展示 4.简述 工程功能可以看这个视频 数码…

微信小程序流量主

开发小程序也已经有一段时间了,也是为了添加流量主来开发小程序,根据小程序的定位,来获取用户想要的资源,通过广告的形式来增加用户的点击量进行收益,收益虽然微不足道,但是也是很有成就感的

活动邀请 | SonarQube×创实信息即将亮相2024 GOPS全球运维大会-上海站,分享代码质量与安全提升策略

2024年10月18日-19日&#xff08;周五-周六&#xff09;&#xff0c;第二十四届 GOPS 全球运维大会上海站将在上海中庚聚龙酒店举办。 大会为期2天&#xff0c;侧重大模型、DevOps、SRE、AIOps、BizDevOps、云原生及安全等热门技术领域。特设了如大模型 运维/研发测试、银行/…

宠物咖啡馆服务平台:SpringBoot技术深度解析

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理基于Spring Boot的宠物咖啡馆平台的设计与…

2024_10_8 系统进展

改进位置 发现是label_api里藏了我需要改进的东西 settings.py 数据库 我这边电脑上使用的是windows 192 vue.config.js 陈家强是这样设置的 module.exports {publicPath: process.env.NODE_ENV production? /: /,assetsDir: static,// css: {// extract: false// },…

使用XML实现MyBatis的基础操作

目录 前言 1.准备工作 1.1⽂件配置 1.2添加 mapper 接⼝ 2.增删改查操作 2.1增(Insert) 2.2删(Delete) 2.3改(Update) 2.4查(Select) 前言 接下来我们会使用的数据表如下&#xff1a; 对应的实体类为&#xff1a;UserInfoMapper 所有的准备工作都在如下文章。 MyBati…

《大规模语言模型从理论到实践》第一轮学习--Fine-tuning微调

第一轮学习目标&#xff1a;了解大模型理论体系 第二轮学习目标&#xff1a;进行具体实操进一步深入理解大模型 从大语言模型的训练过程来理解微调 大预言模型训练主要包含四个阶段&#xff1a;预训练、有监督微调、奖励建模、强化学习。 预训练&#xff08;Pretraining&…