[Python] 关键字yield和return究竟有什麽不同?

学习Scrapy的过程中碰到 yeild 这个关键字,我使用Python快半年了,还真的是第一次遇到这个关键字,於是我花了点时间研究後,终於明白它的作用了,怕下次看到时忘记,所以用这篇文将yield这个关键字重点整理一下。

1. yield的核心目的:为了节省记忆体

如果想要印出0~100的平方时,我们可能会这样写。

powers = [x**2 for x in range(100)]

for x in powers:
    print(x)

但这样有一个致命问题在於,必须把整个list都存放在记忆体中,100个元素可能还不成问题,但如果今天的对象是一百万笔资料,记忆体可能会承受不了,程序就崩溃了。

接下来就会说明yield要如何节省记忆体,但在此之前,先来谈谈Python的生成器(generator)。

2. 什麽是生成器(generator)?

生成器是一个可迭代的物件,可以放在for回圈的in前面,或者使用next()函数呼叫执行下一次迭代。

和列表的差别在於,生成器会保存上次纪录,并只有在呼叫下一层迭代的时候才载入记忆体执行

所以将上面的例子改写成生成器,结果是一样的,却可以防止超过记忆体,注意我用的是 ( 而不是 [

powers = (x**2 for x in range(100))

for x in powers:
    print(x)

3. 函数加入yield後不再是一般的函数,而被视作为生成器(generator)

呼叫函数後,回传的并非数值,而是函数的生成器物件。

4. yield和return一样会回传值,不过yield会记住上次执行的位置

yield和return一样都会回传值并中断在目前位置,但最大不同在於yield在下次迭代时会从上次迭代的下一行接续执行,一直执行到下一个yield出现,如果没有下一个yield则结束这个生成器。而且接续上一个迭代前的变数不会改变,就是维持上次结束前的模样。

这部分我们来看下面这个例子:

def yield_test(n):
    print("start n =", n)
    for i in range(n):
        yield i*i
        print("i =", i)

    print("end")

tests = yield_test(5)
for test in tests:
    print("test =", test)
    print("--------")

执行结果:

start n = 5
test = 0
--------
i = 0
test = 1
--------
i = 1
test = 4
--------
i = 2
test = 9
--------
i = 3
test = 16
--------
i = 4
end
  1. 从第10、11行看到呼叫yield_test()後回传的不是一个数值,而是一个可迭代的生成器。
  2. 在第一次迭代时,印出了 "start n = 5",因为不在回圈中,所以仅仅印出这一次。
  3. 进入回圈中,第一次时 i=0,接着遇到yield并回传 0*0 = 0,并回到主程序。
  4. 主程序的test接收到回传的0,於是印出 "test = 0" 并印出 "--------",结束这次迭代。
  5. 接着进行第二次迭代,会从上次结束的下一行开始,因此印出 "i = 0"。
  6. 完成後又回到回圈开始,这时 i=1,接着再次遇到yield并回传 1*1 = 1,并回到主程序。
  7. 主程序的test接收到回传的1,於是印出 "test = 1" 并印出 "--------",结束这次迭代。
  8. 其他次迭代依此类推,直到i=5跳出回圈,印出 "end" 之後已经没有yield了,生成器会返回一个error StopIteration (这边没有印出来),告诉主程序迭代已经结束了。
  9. 结束主程序。

看完上面例子後,应该会从原本朦朦胧胧到有点概念了吧,其实yield有点像侦错模式的中断点,只是多了中断时回传值而已。

5. next()呼叫下一次迭代,send(n)呼叫下一次迭代并传递参数

def test():
    print("start...")
    while True:
        throw = yield 10
        print("throw:", throw)

p = test()
print(next(p))
print("-----------")
print(next(p))
print("-----------")
print(g.send(7))
print("-----------")

执行结果:

start...
10
-----------
throw: None
10
-----------
throw: 7
10
-----------
  1. 建立一个可迭代生成器p。
  2. next()执行第一次迭代,印出 "start..." 并回传 10,但注意throw在赋予值之前就被中断了。
  3. next()执行第二次迭代,因为throw并没有被没有被赋予值,所以印出 "throw: None",接着回传 10。
  4. send()传入7,等同於在上次结束的位置填入7,因此 throw=7,印出 "throw: 7"。

顺带一提,第一次迭代不可以send任何数值进去,因为没有上一个位置可以接收。

6. Python range小知识

在Python 2.X中,有分range和xrange两种,range就像第一个例子,生成一个[0, 1, 2, ...]的list。xrange则像第二种例子,使用生成器减少记忆体消耗。

但在Python 3.X後range就等於xrange,使用type()检查会知道已经是range型态了。

print(type(range(10)))   # <class 'range'>

如果开始学就是Python3.X,就不必在意这些细节,继续放心地用range吧!

参考


<<:  Rails基本介绍(一)一个实体 && Remove Duplicates from Sorted Array && Remove Element

>>:  MacOS 透过 NVM 管理 Node.js 的版本管理器(Node Version Manager)

DAY14-React Overview

前言: 在经过两个礼拜的内容後,相信大家对写网页也有一定的基础了吧!这两个礼拜我们介绍很了多很好用...

做人如果没梦想,跟咸鱼有什麽区别!

简单自我介绍! 我是一位正在参加五倍红宝石勤奋转职的菜鸟工程师, 这也是我第一次参加IT铁人赛!同时...

Day 5 - 回圈及 ++ --运算式

大家好,我是长风青云。 今天我跟朋友讨论到我参加比赛这件事,她是一个完全没有程序基础的人。 她告诉我...

二、教你怎麽看source code,找到核心程序码 ep.23:Deeplab的model 部署

文章说明 文章分段 文章说明 deeplab的简单介绍、於我的意义 ep.1 tensorflow的...

区块型加密器(cipher block)的操作模式(mode of operation)

.**电子密码本(ECB)**接受纯文本作为输入。 .**密码块链接(CBC)**接受“纯文本XOR...