【Python】一篇文章读懂yield基本用法
这一次,田辛老师想通俗易懂地解释一下Python中的yield功能。
本文要说明以下四个问题:
- yield是什么
- 什么是迭代器和生成器
- yield的基本用法
- 如何使用
yield from
用真正简单的方法讲解yield
并不容易。 我想,就算你不懂yield
语句,也能从我的文档中有所收获。 这篇文章为了让读者理解,举了一个未必特别恰当的例子。 不过例子只是例子,重要的是了解原理。
本文要求环境版本高于Python3.7以上。
1. yield
是什么?
yield
是一种能够暂时中止函数执行的语句。
您可以用它返回此时的返回值并重新启动。 要了解yield
, 你必须了解迭代器和生成器。
ps. 这两句话目前不理解没关系, 后面的例子我们会反复解读这两句话
2. 什么是迭代器和生成器
迭代器:是一种可以迭代以检索元素的类型。 Python的下面list、dict、tuples都可以迭代的,所以这些对象就是迭代器。
生成器:生成器通过每次取出元素执行处理来生成元素。
这里田辛老师先大致说一句为什么yield
这么重要:迭代器在创建可迭代对象的时候, 并不是一次性生成的。 比如我们现在需要一些员工编号的list,如果不用yield
,通常的做法,我们生成一个list,内容是完整的员工编号1-100。 通常是一次性生成,此时内存中就有一个包含着1-100这100个数字的列表。
通常情况下这没什么问题。 但是,一旦我们列表中的内容非常占用内存。 在大数据场景下消耗资源特别大的时候,这就会影响我们程序执行效率以及设备的负荷。 如果你使用了yield
,这就会好很多。 yield
的机制并不会生成一个1-100的序列在那里占内存。 而是只有生成器在要求的时候,这个值才会被生成,而且用过即焚,不用空占内存。
好了,先了解到这里。如果你不理解,我们一边用一边理解。
3. yield
的基本用法
3.1 编写迭代器
yield 基本上是函数内部使用的语句。 基本语法是: yield <value>
我们举一个员工的例子,我们做一个基础编号池,为了简单,这里我给基础编号池3个编号。 这实际上就是迭代器。
def base_code_pool():
"""BASE编号池 1-3 """
for i in range(3):
yield 'BASE-%s' % str(i + 1)
那么这个迭代器怎么使用呢? 还记得田辛老师在最开始的时候说:”yield
是一种能够暂时中止函数执行的语句。您可以用它返回此时的返回值并重新启动。“
也就是我们必须用生成器生成,迭代器才有意义。
3.2 编写生成器
那么我们来做写这个生成器:
gen = base_code_pool()
就这么简单,一行代码,那么如何使用呢?
print(next(gen))
print(next(gen))
print(next(gen))
有了迭代器,有了生成器, 那么我们3次请求这个编号池,输出结果就是:
BASE-1
BASE-2
BASE-3
[Finished in 167ms]
可以看到,每次执行都会产生一个新的编号。
但是这里要注意:迭代器里面多少个元素,就只能请求多少次。 多了会报错。
比如我们再多请求一次。 也就是在刚才的print
代码的部分写四个print(next(gen))
,你会发现前三个正确表示,第四个会报错。田辛老师的执行结果:
BASE-1
BASE-2
BASE-3
Traceback (most recent call last):
File "D:\develop\python\di08-tdd-cdg-python-learning\src\adv_yield\yield_test.py", line 12, in <module>
print(next(gen))
StopIteration
[Finished in 172ms]
这一点要牢记。
这里要说一个小故事, 事实上,Python3及更早版本中存在一个next()
函数, 只不过已经消失。取而代之的是引入了__next__()
方法。看到这个方法的名字我们就知道这是一个特殊方法。 特殊方法在一般程序中大量调用肯定不太好。 于是Python后来的版本又提供了一个next的内置函数。 现在的next()
函数是调用的__next__()
方法。 所以,现在的next()
并非更早期的next()
为了证明这点,我么再举一个例子,还是刚才的BASE迭代器,只不过在使用生成器的时候,这么写:
gen = base_code_pool()
print(gen.__next__()) # 证明了`next()`函数和`__next__()`方法的关系
print(list(gen)) # 证明"BASE-1"已经被释放掉了
这样的执行结果是:
BASE-1
['BASE-2', 'BASE-3']
[Finished in 157ms]
这里,我们首先证明了next()
函数和__next__()
方法的关系。 另外,注意,我们在生成了一个编号后,列出了剩下的全部编号。 你会发现, "BASE-1"并不在这个list里面, 为什么呢? 因为用后即焚,"BASE-1"已经被释放掉了。 这也就是我们一直强调yield
非常节省内存的原因!
4. 关于yield from
现在我们已经学会了如何使用基本的yield
,让我们来谈谈yield from
。
yield from
主要用于将生成器拆分成更小的部分。这是从 Python 3.3 添加的语法,所以它不能在早期版本中使用。
比如刚才的编号是你项目组组员的编号。 公司就给了你三个资源的名额,并且说:人不够就使用外包。 那么在这种情况下, 你的生成器可以做一些调整来适应这个情况。
首先,外包人员也需要一个迭代器:
def outsource_pool():
"""外包编号池:OUTS 1-30"""
for i in range(30): # 好吧,给了你30个外包的名额
yield 'OUTS-%s' % (i + 1)
现在我们有了两个编号池, 我们的原则是先用BASE,BASE的编号用完了以后,就用OUTS。 那么这个逻辑怎么优雅的实现呢? 下面就用到了 yield from
, 参考下面代码
def team_member_code():
"""team_member迭代器"""
yield from base_code_pool()
print('内部资源编号用完,开始使用外包')
yield from outsource_pool()
我们写了一个team_member_code()
迭代器, 这个迭代器里面有两个小迭代器, base写在上面, 外包写在下面。 OK, 我们来调用一下,看看执行结果
team_member = team_member_code()
print(next(team_member))
print(next(team_member))
print(next(team_member))
print(next(team_member))
print(next(team_member))
执行结果是:
BASE-1
BASE-2
BASE-3
内部资源编号用完,开始使用外包
OUTS-1
OUTS-2
[Finished in 138ms]
那么这样,我们就简单的实现了我们刚才组织资源用完了,用外包的需求。
而且这里也证明了,实际上迭代器的执行过程是在yield
的位置中断的。 所以“资源用完”的提示才会出现在子迭代器切换的位置。
我们来试试直接list:print(list(team_member_code()))
, 目的是一次性列出所有编号。
输出结果:
内部资源编号用完,开始使用外包
['BASE-1', 'BASE-2', 'BASE-3', 'OUTS-1', 'OUTS-2', 'OUTS-3', 'OUTS-4', 'OUTS-5', 'OUTS-6', 'OUTS-7', 'OUTS-8', 'OUTS-9', 'OUTS-10', 'OUTS-11', 'OUTS-12', 'OUTS-13', 'OUTS-14', 'OUTS-15', 'OUTS-16', 'OUTS-17', 'OUTS-18', 'OUTS-19', 'OUTS-20', 'OUTS-21', 'OUTS-22', 'OUTS-23', 'OUTS-24', 'OUTS-25', 'OUTS-26', 'OUTS-27', 'OUTS-28', 'OUTS-29', 'OUTS-30']
[Finished in 172ms]
我们可以看到, 如果直接list()
,“资源用完”这句话出现的位置,也说明了他的执行过程。
***特别声明:这个和实际场景有出入,田辛老师作为做了很多年外包项目,也管理了很多年带有外包的团队的IT老兵,没有对外包的同学任何轻视。 这里只是个例子。 ***
5. 总结
本文记录的实际上是yield
的最基础知识。 yield
的基础知识点可以归纳为
- 迭代器和生成器
- 用
yield
时,重复处理使用的内存更少 - 在
for
循环中处理或使用生成器的next()
函数 - 可以利用
yield from
将一个迭代器划分为多个小迭代器,从而进行精细化处理。
6. 代码
老规矩,所有的过程代码附上:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
#-----------------------------------------------------------------------------
# --- TDOUYA STUDIOS ---
#-----------------------------------------------------------------------------
#
# @Project : di08-tdd-cdg-python-learning
# @File : yield_test.py
# @Author : tianxin.xp@gmail.com
# @Date : 2023/2/12 21:57
#
# 用于整理yield使用教学的测试代码
#
#--------------------------------------------------------------------------"""
def base_code_pool():
"""BASE编号池 1-3 """
for i in range(3):
yield 'BASE-%s' % str(i + 1)
# gen = base_code_pool()
# print(next(gen))
# print(next(gen))
# print(next(gen))
# print(next(gen))
# print(gen.__next__())
# print(list(gen))
def outsource_pool():
"""外包编号池:OUTS 1-30"""
for i in range(30): # 好吧,给了你30个外包的名额
yield 'OUTS-%s' % (i + 1)
def team_member_code():
"""team_member迭代器"""
yield from base_code_pool()
print('内部资源编号用完,开始使用外包')
yield from outsource_pool()
# team_member = team_member_code()
# print(next(team_member))
# print(next(team_member))
# print(next(team_member))
# print(next(team_member))
# print(next(team_member))
print(list(team_member_code()))
更多推荐
所有评论(0)