3.2. yield和生成器函数¶
yield是Python编程语言的一个高效的指令性方法。
在Python程序中,使用yield的函数被称作生成器(generator),调用generator时,脚本程序的执行过程与调用普通的函数是完全不同的。
本节课使用几个示例来说明yield的语法和用法。yield几乎是大多数RTOS的线程间切换常用的方法,我们首先了解RTOS的yield,然后在回来 看Python的yield语法。
3.2.1. RTOS中的yield¶
多任务的RTOS(Read Time OS)支持多任务/线程编程模式。假设我们定义2个任务:task_A和task_B。并假设,在task_A任务中,检查是否有按钮A被 按下,如果按钮A被按下,我们在屏幕上显示“A”;task_B任务也是如此,只是检查按钮B是否被按下,如果按下在屏幕上显示“B”。
这样的两个任务,有RTOS编程经验的人会使用yield。在task_A任务中,当未检测到A按钮被按下,则执行yield,OS将立即暂停task_A任务执行,切换 到另一个任务执行。
RTOS多任务切换可以使用yield进行人为干预,提前调度任务切换,提高任务响应性能。
通俗地说,RTOS的yield是当前任务提前放弃计划的任务调度时间,暂停本任务中yield之后的程序执行,当本任务再次被切换回来时,首先执行yield之后 的程序语句。
3.2.2. Python中的yield¶
Python的yield与RTOS的yield很相似。
Python的函数定义中,如果使用yield,这种函数被称作generator。generator都具有next()属性,执行yield语句时,函数会被中断一次,并返回一个 迭代值,下一次再执行该函数时,根据next()属性确定yield的下一个脚本语句,从该语句继续执行函数中的程序语句。从程序执行的流程看,调用generator 时遇到yield就被中断,并返回一个迭代值,再次执行就从yield后的语句开始执行。
Python的generator被yield中断一次并返回一个迭代值,这样的机制带来很多益处,尤其对于需要那些返回列表值且列表非常长的函数,消耗的内存与返回值 列表的长度有关,当系统资源较少时,调用这样的函数会出现内存不足的异常。我们使用yield中断函数执行并返回一个迭代值,下次继续执行yield后的程序, 这样的机制将列表中的值逐个返回,几乎不消耗内存。
下面来对比几个示例,我们可以更好地理解Python的yield。
3.2.3. 返回斐波那契数列的函数¶
斐波那契数列是一个典型的递推器:
x[n] = x[n-1] + x[n-2], n>1, x[0]=1, and x[1]=1
除了前两项,斐波那契数列的第n项等于前两项之和。我们定义一个Python函数Fibonacci生成存储有斐波那契数列前n项的列表,并返回这个列表。代码如下:
1 2 3 4 5 6 7 8 9 10 11 | def Fibonacci(iterm): n, a, b = 0, 0, 1 list_fib = [] while n < iterm: list_fib.append(b) a, b = b, a + b n = n + 1 return list_fib lf = Fibonacci(20) print(lf) |
将该程序代码保存到BlueFi的/CIRCUITPY/code.py文件,你将会在BlueFi的LCD屏幕和串口控制台看到以下输出
这是斐波那契数列前20项的列表。这个程序看起来没有任何毛病,但是我们可以预测,当调用函数Fibonacci的输入参数很大时, 该函数将返回一个很大的列表,占用内存量随着该参数增大而增加。在有限内存资源的计算机系统中调用这个函数,由于函数的 iterm变化而带来不可预制的内存错误。
有经验的Python程序员会建议使用可迭代对象来迭代输出该列表。Python的range()和xrange()都是可迭代器。在Python2.x版本中,这两个迭代器是 不同的,range(n)将生成一个n项整数列表,随着n的增加内存消耗很大;xrange(n)仅仅是一个迭代器,执行一次仅给出一个迭代值,几乎不消耗内存。 在Python3开始,range(n)和xrange(n)完全一样。
BLueFi完全兼容Python3,支持range(n)迭代器,去掉xrange()函数。用USB数据线将BlueFi和电脑连接后,打开MU编辑器, 点击“串口”按钮,按下“Ctrl+c”键并按确认,进入REPL模式,你可以输入验证range()迭代器:
当你将迭代器变量l打印到控制台时,他只是显示“range(0, 10)”,而不是“[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]”,只有使用list(l)函数将 迭代器变量l明确地转换为列表时我们才看到完整列表。
3.2.4. 改进的斐波那契数列生成器¶
模仿Python的range()函数,我们定义一个叫Fibonacci的迭代对象生成器,并使用“for _ in _ ”模版逐一获取斐波那契数列项,代码如下:
1 2 3 4 5 6 7 8 9 | def Fibonacci(iterm): n, a, b = 0, 0, 1 while n < iterm: yield b a, b = b, a + b n = n + 1 for i in Fibonacci(10): print(i) |
将该程序代码保存到BlueFi的/CIRCUITPY/code.py文件,你将会在BlueFi的LCD屏幕和串口控制台看到以下输出:
你可以改变调用Fibonacci生成器时的输入参数,无论你给任意大的数,除了输出数列的打印时间很长之外,内存消耗几乎保持不变。 这个示例程序的关键是第4行——“yield b”,程序执行到这里的时候会中断一次并返回b的当前值,然后再继续执行下一句——继续迭代, 直到while调节不成立。
通过本示例,我们掌握一种新的定义迭代对象的方法,该迭代器依然像“range()”函数一样地使用。
3.2.5. 改进的read_file¶
对文件的读写操作也是Python程序中常用的操作,如果写文件可以用逐“字”增加的方法,那么读文件是否也可以逐“字”读取并处理? 这样的方法跟改进的斐波那契数列生成器一样节约内存,避免将整个文件读入内存再处理。对于有限内存资源的计算机系统来说, 这样地优化读文件操作非常有意义。
1 2 3 4 5 6 7 8 9 | def read_file(file): BLOCK_SIZE = 256 with open(file, 'rb') as f: while True: block = f.read(BLOCK_SIZE) if block: yield block else: return |
这仅仅是一个改进的逐块读取文件的程序模型。关键的第7行——yield block,当程序执行到这里的时候,函数会中断一次并抛出 文件的一个块给函数调用者,然后继续执行下一句,继续读取下一个数据块,如果已经到文件末尾,则直接返回。
采用这个程序模型来读任意大的文件,实际消耗的内存几乎不变,仅与变量BLOCK_SIZE的值有关。
至此,你是否已经掌握Python的yield用法?事实上,yield还有更多种用法可以去探索。