Python 是非常灵活的语言,其中 yield 关键字是普遍容易困惑的概念。

此篇将介绍 yield 关键字,及其相关的概念。

迭代、可迭代、迭代器

迭代(iteration)与可迭代(iterable)

迭代是一种操作;可迭代是对象的一种特性。

很多数据都是「容器」;它们包含了很多其他类型的元素。实际使用容器时,我们常常需要逐个获取其中的元素。逐个获取元素的过程,就是「迭代」

1
2
3
4
# iteration
a_list = [1, 2, 3]
for i in a_list:
    print(i)

如果我们可以从一个对象中,逐个地获取元素,那么我们就说这个对象是「可迭代的」。

Python 中的顺序类型,都是可迭代的(listtuplestring)。其余包括 dictsetfile 也是可迭代的。对于用户自己实现的类型,如果提供了 __iter__() 或者 __getitem__() 方法,那么该类的对象也是可迭代的。

迭代器(iterator)

迭代器是一种对象。

迭代器抽象的是一个「数据流」,是只允许迭代一次的对象。对迭代器不断调用 next() 方法,则可以依次获取下一个元素;当迭代器中没有元素时,调用 next() 方法会抛出 StopIteration 异常。迭代器的 __iter__() 方法返回迭代器自身;因此迭代器也是可迭代的。

迭代器协议(iterator protocol)

迭代器协议指的是容器类需要包含一个特殊方法。

如果一个容器类提供了 __iter__() 方法,并且该方法能返回一个能够逐个访问容器内所有元素的迭代器,则我们说该容器类实现了迭代器协议。

Python 中的迭代器协议和 Python 中的 for 循环是紧密相连的。

1
2
3
# iterator protocol and for loop
for x in something:
    print(x)

Python 处理 for 循环时,首先会调用内建函数 iter(something),它实际上会调用 something.__iter__(),返回 something 对应的迭代器。而后,for 循环会调用内建函数 next(),作用在迭代器上,获取迭代器的下一个元素,并赋值给 x。此后,Python 才开始执行循环体。

生成器、yield 表达式

生成器函数(generator function)和生成器(generator)

生成器函数是一种特殊的函数;生成器则是特殊的迭代器。

如果一个函数包含 yield 表达式,那么它是一个生成器函数;调用它会返回一个特殊的迭代器,称为生成器。

1
2
3
4
5
6
7
8
9
10
11
def func():
    return 1

def gen():
    yield 1

print(type(func))   # <class 'function'>
print(type(gen))    # <class 'function'>

print(type(func())) # <class 'int'>
print(type(gen()))  # <class 'generator'>

如上,生成器 gen 看起来和普通的函数没有太大区别。仅只是将 return 换成了 yield。用 type() 函数打印二者的类型也能发现,func 和 gen 都是函数。然而,二者的返回值的类型就不同了。func() 是一个 int 类型的对象;而 gen() 则是一个迭代器对象。

yield 表达式

如前所述,如果一个函数定义中包含 yield 表达式,那么该函数是一个生成器函数(而非普通函数)。实际上,yield 仅能用于定义生成器函数。

与普通函数不同,生成器函数被调用后,其函数体内的代码并不会立即执行,而是返回一个生成器(generator-iterator)。当返回的生成器调用成员方法时,相应的生成器函数中的代码才会执行。

1
2
3
4
5
6
def square():
    for x in range(4):
        yield x ** 2
square_gen = square()
for x in square_gen:
    print(x)

前面说到,for 循环会调用 iter() 函数,获取一个生成器;而后调用 next() 函数,将生成器中的下一个值赋值给 x;再执行循环体。因此,上述 for 循环基本等价于:

1
2
3
4
genitor = square_gen.__iter__()
while True:
    x = geniter.next() # Python 3 是 __next__()
    print(x)

注意到,square 是一个生成器函数;作为它的返回值,square_gen 已经是一个迭代器;迭代器的 __iter__() 返回它自己。因此 geniter 对应的生成器函数,即是 square

每次执行到 x = geniter.next() 时,square 函数会从上一次暂停的位置开始,一直执行到下一个 yield 表达式,将 yield 关键字后的表达式列表返回给调用者,并再次暂停。注意,每次从暂停恢复时,生成器函数的内部变量、指令指针、内部求值栈等内容和暂停时完全一致

生成器的方法

生成器有一些方法。调用这些方法可以控制对应的生成器函数;不过,若是生成器函数已在执行过程中,调用这些方法则会抛出 ValueError 异常。

  • generator.next():从上一次在 yield 表达式暂停的状态恢复,继续执行到下一次遇见 yield 表达式。当该方法被调用时,当前 yield 表达式的值为 None,下一个 yield 表达式中的表达式列表会被返回给该方法的调用者。若没有遇到 yield 表达式,生成器函数就已经退出,那么该方法会抛出 StopIterator 异常。
  • generator.send(value):和 generator.next() 类似,差别仅在与它会将当前 yield 表达式的值设置为 value
  • generator.throw(type[, value[, traceback]]):向生成器函数抛出一个类型为 type 值为 value 调用栈为 traceback 的异常,而后让生成器函数继续执行到下一个 yield 表达式。其余行为与 generator.next() 类似。
  • generator.close():告诉生成器函数,当前生成器作废不再使用。

举例和说明

如果你看不懂生成器函数

如果你还是不太能理解生成器函数,那么大致上你可以这样去理解。

  • 在函数开始处,加入 result = list()
  • 将每个 yield 表达式 yield expr 替换为 result.append(expr)
  • 在函数末尾处,加入 return result

关于「下一个」yield 表达式

介绍「生成器的方法」时,我们说当调用 generator.next() 时,生成器函数会从当前位置开始执行到下一个 yield 表达式。这里的「下一个」指的是执行逻辑的下一个。因此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def f123():
    yield 1
    yield 2
    yield 3

for item in f123(): # 1, 2, and 3, will be printed
    print(item)

def f13():
    yield 1
    while False:
        yield 2
    yield 3

for item in f13(): # 1 and 3, will be printed
    print(item)

使用 send() 方法与生成器函数通信

1
2
3
4
5
6
7
8
9
10
def func():
    x = 1
    while True:
        y = (yield x)
        x += y

geniter = func()
geniter.next()  # 1
geniter.send(3) # 4
geniter.send(10)# 14

此处,生成器函数 func 用 yield 表达式,将处理好的 x 发送给生成器的调用者;与此同时,生成器的调用者通过 send 函数,将外部信息作为生成器函数内部的 yield 表达式的值,保存在 y 当中,并参与后续的处理。

这一特性是使用 yield 在 Python 中使用协程的基础。

yield 的好处

Python 的老用户应该会熟悉 Python 2 中的一个特性:内建函数 range 和 xrange。其中,range 函数返回的是一个列表,而 xrange 返回的是一个迭代器。

在 Python 3 中,range 相当于 Python 2 中的 xrange;而 Python 2 中的 range 可以用 list(range()) 来实现。

Python 之所以要提供这样的解决方案,是因为在很多时候,我们只是需要逐个顺序访问容器内的元素。大多数时候,我们不需要「一口气获取容器内所有的元素」。比方说,顺序访问容器内的前 5 个元素,可以有两种做法:

  • 获取容器内的所有元素,然后取出前 5 个;
  • 从头开始,逐个迭代容器内的元素,迭代 5 个元素之后停止。

显而易见,如果容器内的元素数量非常多(比如有 10 ** 8 个),或者容器内的元素体积非常大,那么后一种方案能节省巨大的时间、空间开销。

现在假设,我们有一个函数,其产出(返回值)是一个列表。而若我们知道,调用者对该函数的返回值,只有逐个迭代这一种方式。那么,如果函数生产列表中的每一个元素都需要耗费非常多的时间,或者生成所有元素需要等待很长时间,则使用 yield把函数变成一个生成器函数,每次只产生一个元素,就能节省很多开销了。

Python 中的黑暗角落(一):理解 yield 关键字相关推荐

  1. Python天天美味(25) - 深入理解yield

    yield的英文单词意思是生产,刚接触Python的时候感到非常困惑,一直没弄明白yield的用法.只是粗略的知道yield可以用来为一个函数返回值塞数据,比如下面的例子: def  addlist( ...

  2. python中比较重要的几个函数_Python 几个重要的内置函数 python中的内置函数和关键字需要背过吗...

    python重要的几个内置函数用法 python内置函数什么用忘不掉的是回忆,继续的是生活,错过的,就当是路过吧.来来往往身边出现很多人,总有一个位置,一直没有变.看看温暖的阳光,偶尔还是会想一想. ...

  3. python self 值自动改变,在python中对self的理解

    在python中对self的理解 : 一.self的位置是出现在哪里? 首先,self是在类的方法中的,在调用此方法时,不用给self赋值,Python会自动给他赋值,而且这个值就是类的实例--对象本 ...

  4. Python中if __name__=='__main__': 理解与总结(看这篇就够了,一文扫清疑惑!)

    前言 在Python当中,如果代码写得规范一些,通常会写上一句if '__name__'=='__main__:'作为程序的入口,但似乎没有这么一句代码,程序也能正常运行.这句代码多余吗?原理又在哪里 ...

  5. python中定义类的关键字_在Python中,定义一个类使用什么关键字?

    [多选题]技术应用的限制包括 [多选题]关于类和对象,下面说法正确的有? [判断题]温度越高,料液的粘度越小,扩散系数越大,可提高膜通量. [单选题]某企业有10台运货车,已知每台车每运行100小时平 ...

  6. Python中timedelta类型的理解

    Python中timedelta类型的理解 逻辑: timedelta = datetime1-datetime2 理解:一个时间等于两个时刻做差 代码 import datetimeif __nam ...

  7. Python 中的黑暗角落(三):模块与包

    如果你用过 Python,那么你一定用过 import 关键字加载过各式各样的模块.但你是否熟悉 Python 中的模块与包的概念呢?或者,以下几个问题,你是否有明确的答案? 什么是模块?什么又是包? ...

  8. Python 中的黑暗角落(二):生成器协程的调度问题

    前作介绍了 Python 中的 yield 关键字.此篇介绍如何使用 yield 表达式,在 Python 中实现一个最基本的协程调度示例,避免 I/O 操作占用大量 CPU 计算时间. 协程及其特点 ...

  9. python中的引用怎么理解_python 引用和对象理解

    今天浏览博客的时候看到这么一句话: python中变量名和对象是分离的:最开始的时候是看到这句话的时候没有反应过来.决定具体搞清楚一下python中变量与对象之间的细节.(其实我感觉应该说 引用和对象 ...

最新文章

  1. UVA 10129 Play on Words(欧拉道路)
  2. 工具类:BeanUtils和PropertyUtils的区别
  3. AIX 3D32B80D 错误
  4. 社交产品后端架构设计--转载
  5. 5.intent_activity
  6. 【深度学习】网络架构设计:CNN based和Transformer based
  7. 在服务器上安装网站环境,如何在服务器上搭建JSP环境,需要安装哪些软件?
  8. Java 进阶—— super 和 this 的用法
  9. springboot html压缩,springboot 请求响应压缩
  10. Windows 服务程序编写
  11. myeclpse 8.5 小问题记录
  12. php redis 里面的hscan 第四个参数count很不靠谱
  13. c++重复代码检查工具
  14. 多变量微积分笔记20——球坐标系
  15. BLE蓝牙应用生成Android/iOS APP以及小程序
  16. 磁盘与文件系统管理详解
  17. 如何实现 水平/垂直居中
  18. git remote add origin xxx.git 的问题解决
  19. 密码学之DES/AES算法
  20. Java实现TCP通讯

热门文章

  1. 世界坐标系到观察坐标系的变换步骤_shader观察(像机)矩阵变换的一己之见
  2. 这才是牛逼程序员的标配!
  3. 几个年薪百万的下属,爆了~
  4. 语音识别技术的前世今生
  5. 10行Python代码搞定目标检测
  6. snmp协议_SNMP开发和使用
  7. 计算机网络中的node是指_信息工程及计算机网络技术简介
  8. 马斯克:大部分人没必要活那么长!活太久会让社会“陷入窒息”
  9. 继爱奇艺之后,腾讯视频、优酷宣布:取消剧集超前点播服务
  10. 顺丰同城宣布苏炳添出任首位品牌代言人