百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Python 进阶——如何正确使用 yield

itomcoil 2024-12-14 12:43 22 浏览

本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理

在 Python 开发中, yield 关键字的使用其实较为频繁,例如大集合的生成,简化代码结构、协程与并发都会用到它。


但是,你是否真正了解 yield 的运行过程呢?


这篇文章,我们就来看一下 yield 的运行流程,以及在开发中哪些场景适合使用 yield 。


生成器


如果在一个方法内,包含了 yield 关键字,那么这个函数就是一个「生成器」。


生成器其实就是一个特殊的迭代器,它可以像迭代器那样,迭代输出方法内的每个元素。?

我们来看一个包含 yield 关键字的方法:


# coding: utf8

# 生成器
defgen(n):
foriinrange(n):
yieldi

g = gen(5)# 创建一个生成器
print(g)# <generator object gen at 0x10bb46f50>
print(type(g))# <type 'generator'>

# 迭代生成器中的数据
foriing:
print(i)

# Output:
# 0 1 2 3 4


注意,在这个例子中,当我们执行 g = gen(5) 时, gen 中的代码其实并没有执行,此时我们只是创建了一个「生成器对象」,它的类型是 generator 。


然后,当我们执行 for i in g ,每执行一次循环,就会执行到 yield 处,返回一次 yield 后面的值。


这个迭代过程是和迭代器最大的区别。


换句话说,如果我们想输出 5 个元素,在创建生成器时,这个 5 个元素其实还并没有产生,什么时候产生呢?只有在执行 for 循环遇到 yield 时,才会依次生成每个元素。


此外,生成器除了和迭代器一样实现迭代数据之外,还包含了其他方法:


  • generator.__next__() :执行 for 时调用此方法,每次执行到 yield 就会停止,然后返回 yield 后面的值,如果没有数据可迭代,抛出 StopIterator 异常, for 循环结束
  • generator.send(value) :外部传入一个值到生成器内部,改变 yield 前面的值
  • generator.throw(type[, value[, traceback]]) :外部向生成器抛出一个异常
  • generator.close() :关闭生成器


通过使用生成器的这些方法,我们可以完成很多有意思的功能。

__ next __


先来看生成器的 __next__ 方法,我们看下面这个例子。


# coding: utf8

defgen(n):
foriinrange(n):
print('yield before')
yieldi
print('yield after')

g = gen(3)# 创建一个生成器
print(g.__next__())# 0
print('----')
print(g.__next__())# 1
print('----')
print(g.__next__())# 2
print('----')
print(g.__next__())# StopIteration

# Output:
# yield before
# 0
# ----
# yield after
# yield before
# 1
# ----
# yield after
# yield before
# 2
# ----
# yield after
# Traceback (most recent call last):
# File "gen.py", line 16, in <module>
# print(g.__next__()) # StopIteration
# StopIteration


在这个例子中,我们定义了 gen 方法,这个方法包含了 yield 关键字。然后我们执行 g = gen(3) 创建一个生成器,但是这次没有执行 for 去迭代它,而是多次调用 g.next() 去输出生成器中的元素。


我们看到,当执行 g.__next__() 时,代码就会执行到 yield 处,然后返回 yield 后面的值,如果继续调用 g.__next__() ,注意,你会发现,这次执行的开始位置,是上次 yield 结束的地方,并且它还保留了上一次执行的上下文,继续向后迭代。


这就是使用 yield 的作用,在迭代生成器时,每一次执行都可以保留上一次的状态,而不是像普通方法那样,遇到 return 就返回结果,下一次执行只能再次重复上一次的流程。


生成器除了能保存状态之外,我们还可以通过其他方式,改变其内部的状态,这就是下面要讲的 send 和 throw 方法。


send


上面的例子中,我们只展示了在 yield 后有值的情况,其实还可以使用 j = yield i 这种语法,我们看下面的代码:


# coding: utf8

defgen():
i =1
whileTrue:
j =yieldi
i *=2
ifj ==-1:
break


此时如果我们执行下面的代码:


foriingen():
print(i)
time.sleep(1)


输出结果会是 1 2 4 8 16 32 64 ... 一直循环下去, 直到我们杀死这个进程才能停止。


这段代码一直循环的原因在于,它无法执行到 j == -1 这个分支里 break 出来,如果我们想让代码执行到这个地方,如何做呢?


这里就要用到生成器的 send 方法了, send 方法可以把外部的值传入生成器内部,从而改变生成器的状态。


代码可以像下面这样写:


g = gen()# 创建一个生成器
print(g.__next__())# 1
print(g.__next__())# 2
print(g.__next__())# 4
# send 把 -1 传入生成器内部 走到了 j = -1 这个分支
print(g.send(-1))# StopIteration 迭代停止


当我们执行 g.send(-1) 时,相当于把 -1 传入到了生成器内部,然后赋值给了 yield 前面的 j ,此时 j = -1 ,然后这个方法就会 break 出来,不会继续迭代下去。


throw


外部除了可以向生成器内部传入一个值外,还可以传入一个异常,也就是调用 throw 方法:


# coding: utf8

defgen():
try:
yield1
exceptValueError:
yield'ValueError'
finally:
print('finally')

g = gen()# 创建一个生成器
print(g.__next__())# 1
# 向生成器内部传入异常 返回ValueError
print(g.throw(ValueError))

# Output:
# 1
# ValueError
# finally


这个例子创建好生成器后,使用 g.throw(ValueError) 的方式,向生成器内部传入了一个异常,走到了生成器异常处理的分支逻辑。


close


生成器的 close 方法也比较简单,就是手动关闭这个生成器,关闭后的生成器无法再进行操作。


>>>g = gen()
>>>g.close()# 关闭生成器
>>>g.__next__()# 无法迭代数据
Traceback (most recent call last):
File"<stdin>", line1,in<module>
StopIteration


close 方法我们在开发中使用得比较少,了解一下就好。


使用场景


了解了 yield 和生成器的使用方式,那么 yield 和生成器一般用在哪些业务场景中呢?


下面我介绍几个例子,分别是大集合的生成、简化代码结构、协程与并发,你可以参考这些使用场景来使用 yield 。


大集合的生成


如果你想生成一个非常大的集合,如果使用 list 创建一个集合,这会导致在内存中申请一个很大的存储空间,例如想下面这样:


# coding: utf8

defbig_list():
result = []
foriinrange(10000000000):
result.append(i)
returnresult

# 一次性在内存中生成大集合 内存占用非常大
foriinbig_list():
print(i)


这种场景,我们使用生成器就能很好地解决这个问题。


因为生成器只有在执行到 yield 时才会迭代数据,这时只会申请需要返回元素的内存空间,代码可以这样写:


# coding: utf8

defbig_list():
foriinrange(10000000000):
yieldi

# 只有在迭代时 才依次生成元素 减少内存占用
foriinbig_list():
print(i)


简化代码结构


我们在开发时还经常遇到这样一种场景,如果一个方法要返回一个 list ,但这个 list 是多个逻辑块组合后才能产生的,这就会导致我们的代码结构变得很复杂:


# coding: utf8

defgen_list():
# 多个逻辑块 组成生成一个列表
result = []
foriinrange(10):
result.append(i)
forjinrange(5):
result.append(j * j)
forkin[100,200,300]:
result.append(k)
returnresult

foritemingen_list():
print(item)


这种情况下,我们只能在每个逻辑块内使用 append 向 list 中追加元素,代码写起来比较啰嗦。


此时如果使用 yield 来生成这个 list ,代码就简洁很多:


# coding: utf8

defgen_list():
# 多个逻辑块 使用yield 生成一个列表
foriinrange(10):
yieldi
forjinrange(5):
yieldj * j
forkin[100,200,300]:
yieldk

foritemingen_list():
print(i)


使用 yield 后,就不再需要定义 list 类型的变量,只需在每个逻辑块直接 yield 返回元素即可,可以达到和前面例子一样的功能。


我们看到,使用 yield 的代码更加简洁,结构也更清晰,另外的好处是只有在迭代元素时才申请内存空间,降低了内存资源的消耗。


协程与并发


还有一种场景是 yield 使用非常多的,那就是「协程与并发」。


如果我们想提高程序的执行效率,通常会使用多进程、多线程的方式编写程序代码,最常用的编程模型就是「生产者-消费者」模型,即一个进程 / 线程生产数据,其他进程 / 线程消费数据。


在开发多进程、多线程程序时,为了防止共享资源被篡改,我们通常还需要加锁进行保护,这样就增加了编程的复杂度。


在 Python 中,除了使用进程和线程之外,我们还可以使用「协程」来提高代码的运行效率。


什么是协程?


简单来说, 由多个程序块组合协作执行的程序,称之为「协程」。


而在 Python 中使用「协程」,就需要用到 yield 关键字来配合。


可能这么说还是太好理解,我们用 yield 实现一个协程生产者、消费者的例子:


# coding: utf8

defconsumer():
i =None
whileTrue:
# 拿到 producer 发来的数据
j =yieldi
print('consume %s'% j)

defproducer(c):
c.__next__()
foriinrange(5):
print('produce %s'% i)
# 发数据给 consumer
c.send(i)
c.close()

c = consumer()
producer(c)

# Output:
# produce 0
# consume 0
# produce 1
# consume 1
# produce 2
# consume 2
# produce 3
# consume 3
...


这个程序的执行流程如下:


  1. c = consumer() 创建一个生成器对象
  2. producer(c) 开始执行, c.__next()__ 会启动生成器 consumer 直到代码运行到 j = yield i 处,此时 consumer 第一次执行完毕,返回
  3. producer 函数继续向下执行,直到 c.send(i) 处,这里利用生成器的 send 方法,向 consumer 发送数据
  4. consumer 函数被唤醒,从 j = yield i 处继续开始执行,并且接收到 producer 传来的数据赋值给 j ,然后打印输出,直到再次执行到 yield 处,返回
  5. producer 继续循环执行上面的过程,依次发送数据给 cosnumer ,直到循环结束
  6. 最终 c.close() 关闭 consumer 生成器,程序退出


在这个例子中我们发现,程序在 producer 和 consumer 这 2 个函数之间 来回切换 执行,相互协作,完成了生产任务、消费任务的业务场景,最重要的是,整个程序是在 单进程单线程 下完成的。


这个例子用到了上面讲到的 yield 、生成器的 __next__ 、 send 、 close 方法。如果不好理解,你可以多看几遍这个例子,最好自己测试一下。


我们使用协程编写生产者、消费者的程序时,它的好处是:


  • 整个程序运行过程中无锁,不用考虑共享变量的保护问题,降低了编程复杂度
  • 程序在函数之间来回切换,这个过程是用户态下进行的,不像进程 / 线程那样,会陷入到内核态,这就减少了内核态上下文切换的消耗,执行效率更高


所以, Python 的 yield 和生成器实现了协程的编程方式,为程序的并发执行提供了编程基础。


Python 中的很多第三方库,都是基于这一特性进行封装的,例如 gevent 、 tornado ,它们都大大提高了程序的运行效率。


总结


总结一下,这篇文章我们主要讲了 yield 的使用方式,以及生成器的各种特性。


生成器是一种特殊的迭代器,它除了可以迭代数据之外,在执行时还可以保存方法中的状态,除此之外,它还提供了外部改变内部状态的方式,把外部的值传入到生成器内部。


利用 yield 和生成器的特性,我们在开发中可以用在大集成的生成、简化代码结构、协程与并发的业务场景中。


Python 的 yield 也是实现协程和并发的基础,它提供了协程这种用户态的编程模式,提高了程序运行的效率。

私信小编01即可获取大量Python学习资料

相关推荐

PS小技巧 调整命令,让人物肤色变得更加白皙 #后期修图

我们来看一下如何去将人物的皮肤变得更加的白皙。·首先选中图层,Ctrl键加J键复制一层。·打开这里的属性面板,选择快速操作删除背景,这样就会将人物进行单独的抠取。·接下来在上方去添加一个黑白调整图层,...

把人物肤色提亮的方法和技巧

PS后期调白肤色提亮照片的方法。一白遮百丑,所以对于Photoshop后期来说把人物肤色调白是一项非常重要的任务。就拿这张素材图片来说,这张素材图片人脸的肤色主要偏红、偏黄,也不够白皙,该怎样对它进行...

《Photoshop教程》把美女图片调成清爽色彩及润肤技巧

关注PS精品教程,每天不断更新~~室内人物图片一般会偏暗,人物脸部、肤色及背景会出现一些杂点。处理之前需要认真的给人物磨皮及美白,然后再整体润色。最终效果原图一、用修补工具及图章工具简单去除大一点的黑...

PS后期对皮肤进行美白的技巧

PS后期进行皮肤美白的技巧。PS后期对皮肤进行美白的技巧:·打开素材图片之后直接复制原图。·接下来直接点击上方的图像,选择应用图像命令。·在通道这里直接选择红通道,混合这里直接选择柔光,然后点击确定。...

493 [PS调色]调模特通透肤色

效果对比:效果图吧:1、光位图:2、拍摄参数:·快门:160;光圈:8;ISO:1003、步骤分解图:用曲线调整图层调出基本色调。用可选颜色调整图层调整红色、黄色、白色和灰色4种颜色的混合比例。用色彩...

先选肤色再涂面部,卡戴珊的摄影师透露:为明星拍完照后怎么修图

据英国媒体12月17日报道,真人秀明星金·卡戴珊终于承认,她把女儿小北P进了家族的圣诞贺卡,怪不得粉丝们都表示这张贺卡照得非常失败。上周,这位39岁的女星遭到了一些粉丝针对这张照片的批评,她于当地时间...

如何在PS中运用曲线复制另一张照片的色调

怎样把另一张作品的外观感觉,套用到自己的照片上?单靠肉眼来猜,可能很不容易,而来自BenSecret的教学,关键是在PS使用了两个工具,让你可以准确比较两张照片的曝光、色调与饱和度,方便你调整及复制...

PS在LAB模式下调出水嫩肤色的美女

本PS教程主要使用Photoshop使用LAB模式调出水嫩肤色的美女,教程调色比较独特。作者比较注重图片高光部分的颜色,增加质感及肤色调红润等都是在高光区域完成。尤其在Lab模式下,用高光选区调色后图...

在Photoshop图像后期处理中如何将人物皮肤处理得白皙通透

我们在人像后期处理中,需要将人物皮肤处理的白皙通透,处理方法很多,大多数都喜欢使用曲线、磨皮等进行调整,可以达到亮但是不透,最终效果往往不是很好,今天就教大家一种如何将任务皮肤处理得白皙通透,希望能帮...

PS调色自学教程:宝宝照片快速调通透,简单实用!

PS调色自学教程:宝宝照片快速调通透。·首先复制图层,然后选择进入ACR滤镜,选择曲线锁定照片的亮部,也就高光位置,其他部位补亮一点,尤其是阴影的部位补亮多一些,让画面的层次均匀一点。·然后回到基本项...

【干货】如何利用PS进行人物美化

人物图像美化在Photoshop中非常常用,Photoshop作为一款功能强大的图像处理软件,不仅可以对人像进行基本的调色、美化和修复等处理,还可以改变人物的线条和幅度,如调整脸部器官和脸型的大小、调...

教大家一种可以快速把肤色处理均匀的方法@抖音短视频

快速把肤色处理均匀的方法。今天教大家一种可以快速把肤色处理均匀的方法。像这张照片整体肤色走紫红色,但是局部偏黄缘处理起来非常的麻烦。其实我们只需要新建空白图层,图层混合模式更改为颜色,再选择画笔工具把...

PS调色教程 利用RAW调出干净通透的肤色

要么不发,要么干货。后期教程来噜~用RAW调出干净通透的肤色。这次终于不会原片比PS后好看了吧。如果你依然这么觉得,请不要残忍的告诉我这个事实,泪谢TAT)附送拍摄花絮,感谢各位的支持更多风格请关注m...

photoshop后期皮肤变白的技巧

PS后期皮肤变白的技巧。1.PS后期让皮肤变白的方法有很多种,接下来教你一种非常简单容易上手的方法。2.打开素材图片之后,直接在小太极下拉框的位置添加一个纯色调整图层,颜色设置一个纯白色,点击...

Photoshop调出人物的淡雅粉嫩肤色教程

本教程主要使用Photoshop调出人物的淡雅粉嫩肤色教程,最终的效果非常的通透迷人,下面让我们一起来学习.出自:86ps效果图:原图:1、打开原图复制一层。2、用Topaz滤镜磨皮(点此下载)。3、...