这一次,田辛老师想通俗易懂地解释一下Python中的yield功能。

本文要说明以下四个问题:

  1. yield是什么
  2. 什么是迭代器和生成器
  3. yield的基本用法
  4. 如何使用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的基础知识点可以归纳为

  1. 迭代器和生成器
  2. yield 时,重复处理使用的内存更少
  3. for 循环中处理或使用生成器的next()函数
  4. 可以利用 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()))

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐