Python 进阶——如何正确使用 yield?
本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理
在 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
...
这个程序的执行流程如下:
c = consumer()
创建一个生成器对象producer(c)
开始执行,c.__next()__
会启动生成器consumer
直到代码运行到j = yield i
处,此时consumer
第一次执行完毕,返回producer
函数继续向下执行,直到c.send(i)
处,这里利用生成器的send
方法,向consumer
发送数据consumer
函数被唤醒,从j = yield i
处继续开始执行,并且接收到producer
传来的数据赋值给j
,然后打印输出,直到再次执行到yield
处,返回producer
继续循环执行上面的过程,依次发送数据给cosnumer
,直到循环结束最终
c.close()
关闭consumer
生成器,程序退出
在这个例子中我们发现,程序在 producer
和 consumer
这 2 个函数之间 来回切换 执行,相互协作,完成了生产任务、消费任务的业务场景,最重要的是,整个程序是在 单进程单线程 下完成的。
这个例子用到了上面讲到的 yield
、生成器的 __next__
、 send
、 close
方法。如果不好理解,你可以多看几遍这个例子,最好自己测试一下。
我们使用协程编写生产者、消费者的程序时,它的好处是:
整个程序运行过程中无锁,不用考虑共享变量的保护问题,降低了编程复杂度
程序在函数之间来回切换,这个过程是用户态下进行的,不像进程 / 线程那样,会陷入到内核态,这就减少了内核态上下文切换的消耗,执行效率更高
所以, Python 的 yield
和生成器实现了协程的编程方式,为程序的并发执行提供了编程基础。
Python 中的很多第三方库,都是基于这一特性进行封装的,例如 gevent
、 tornado
,它们都大大提高了程序的运行效率。
总结
总结一下,这篇文章我们主要讲了 yield
的使用方式,以及生成器的各种特性。
生成器是一种特殊的迭代器,它除了可以迭代数据之外,在执行时还可以保存方法中的状态,除此之外,它还提供了外部改变内部状态的方式,把外部的值传入到生成器内部。
利用 yield
和生成器的特性,我们在开发中可以用在大集成的生成、简化代码结构、协程与并发的业务场景中。
Python 的 yield
也是实现协程和并发的基础,它提供了协程这种用户态的编程模式,提高了程序运行的效率。
想要获取更多Python学习资料可以加
QQ:2955637827私聊
或加Q群630390733
大家一起来学习讨论吧!
Python 进阶——如何正确使用 yield?的更多相关文章
- Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就绪,挂起,运行) ,***协程概念,yield模拟并发(有缺陷),Greenlet模块(手动切换),Gevent(协程并发)
Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就 ...
- python进阶篇
python进阶篇 import 导入模块 sys.path:获取指定模块搜索路径的字符串集合,可以将写好的模块放在得到的某个路径下,就可以在程序中import时正确找到. import sys ...
- Python进阶-继承中的MRO与super
Python进阶-继承中的MRO与super 写在前面 如非特别说明,下文均基于Python3 摘要 本文讲述Python继承关系中如何通过super()调用"父类"方法,supe ...
- python进阶学习笔记(一)
python进阶部分要学习的内容: 学习目标: 1.函数式编程 1.1,什么是函数式编程 函数式编程是一种抽象计算的编程模式 不同语言的抽象层次不同: 函数式编程的特点: python支持的函数式编程 ...
- Python进阶 函数式编程和面向对象编程等
函数式编程 函数:function 函数式:functional,一种编程范式.函数式编程是一种抽象计算机的编程模式. 函数!= 函数式(如计算!=计算机) 如下是不同语言的抽象 层次不同 高阶函数: ...
- 【python进阶】深入理解系统进程2
前言 在上一篇[python进阶]深入理解系统进程1中,我们讲述了多任务的一些概念,多进程的创建,fork等一些问题,这一节我们继续接着讲述系统进程的一些方法及注意点 multiprocessing ...
- Python进阶:全面解读高级特性之切片!
导读:切片系列文章连续写了三篇,本文是对它们做的汇总.为什么要把序列文章合并呢?在此说明一下,本文绝不是简单地将它们做了合并,主要是修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔 ...
- Python进阶:迭代器与迭代器切片
2018-12-31 更新声明:切片系列文章本是分三篇写成,现已合并成一篇.合并后,修正了一些严重的错误(如自定义序列切片的部分),还对行文结构与章节衔接做了大量改动.原系列的单篇就不删除了,毕竟也是 ...
- Python进阶之面向对象编程
面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想.OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数. 面向过程的程序设计把计算机 ...
随机推荐
- VMware与Device/Credential Guard不兼容问题
启动虚拟机vmware突然报不兼容错误 解决方法: 1首先打开控制面板>程序>启动或关闭Windows功能, 取消Hyper-v的勾选 2.在往下划,关闭Windows沙盒的勾选沙盒和虚拟 ...
- CodeChef-LECOINS Little Elephant and Colored Coins 题解
CodeChef-LECOINS Little Elephant and Colored Coins Little Elephant and Colored Coins The Little Elep ...
- SpringBoot整合Elasticsearch7
SpringBoot连接ElasticSearch有以下种方式, TransportClient,9300端口,在 7.x 中已经被弃用,据说在8.x 中将完全删除 restClient,9200端口 ...
- 编程C语言进阶篇——自定义数据类型:结构体
一.结构体 定义方法: 结构名 变量名 特点: 两个同类型的结构变量可以相互赋值,但是结构变量之间不能使用"<","=="等运算符,如果使用则需要对运算符 ...
- Kafka源码环境搭建
github地址:https://github.com/apache/kafka clone下来之后可以看到这样的项目结构: 文件目录说明. 目录 描述 bin Windows 和 Linux 下 K ...
- java43
自定义日期格式 import java.text.DateFormat; import java.text.ParseException; import java.util.Date; public ...
- SQL优化之SQL 进阶技巧(下)
上文( SQL优化之SQL 进阶技巧(上) )我们简述了 SQL 的一些进阶技巧,一些朋友觉得不过瘾,我们继续来下篇,再送你 10 个技巧 一. 使用延迟查询优化 limit [offset], [r ...
- Docker学习—Stack
前言: 前一篇了解Docker使用Swarm集群部署方式,并创建服务到Swarm集群中:如果在集群部署过程中存在大量服务部署.编排那么该如何处理呢? 那么就需要了解Docker Stack了. 1.D ...
- Log4net 的 ASP.NET Core 扩展库
给大家安利一款 log4net 的 ASP.NET Core 扩展库,它是基于 log4net 开发的. 简单易用,开源免费,使用ASP.NET Core自身提供的DI容器来实现服务的注册和消费.直接 ...
- 直接插入排序(python实现)
这篇博文用来介绍直接插入排序 直接插入排序基本思想: 每次将一个待排序的记录插入到已经排好序的数据区中,直到全部插入完为止 直接插入排序算法思路: 在直接插入排序中,数据元素分为了有序区和无序区两个部 ...