Python yield

Python yield使用浅析

廖雪峰 / 编程 / 2012-11-22 18:34 / Reads: 14

本文最早发表于IBM developerWorks: Python yield 使用浅析

您可能听说过,带有 yield 的函数在 Python 中被称之为 generator(生成器),何谓 generator ?

我们先抛开 generator,以一个常见的编程题目来展示 yield 的概念。

如何生成斐波那契數列

斐波那契(Fibonacci)數列是一个非常简单的递归数列,除第一个和第二个数外,任意一个数都可由前两个数相加得到。用计算机程序输出斐波那契數列的前 N 个数是一个非常简单的问题,许多初学者都可以轻易写出如下函数:

清单 1. 简单输出斐波那契數列前 N 个数

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        print b
        a, b = b, a + b
        n = n + 1

执行 fab(5),我们可以得到如下输出:

>>> fab(5)
1
1
2
3
5

结果没有问题,但有经验的开发者会指出,直接在 fab 函数中用 print 打印数字会导致该函数可复用性较差,因为 fab 函数返回 None,其他函数无法获得该函数生成的数列。

要提高 fab 函数的可复用性,最好不要直接打印出数列,而是返回一个 List。以下是 fab 函数改写后的第二个版本:

清单 2. 输出斐波那契數列前 N 个数第二版

def fab(max): 
    n, a, b = 0, 0, 1 
    L = [] 
    while n < max: 
        L.append(b) 
        a, b = b, a + b 
        n = n + 1 
    return L 

可以使用如下方式打印出 fab 函数返回的 List:

>>> for n in fab(5):
...     print n
...
1
1
2
3
5

改写后的 fab 函数通过返回 List 能满足复用性的要求,但是更有经验的开发者会指出,该函数在运行中占用的内存会随着参数 max 的增大而增大,如果要控制内存占用,最好不要用 List 来保存中间结果,而是通过 iterable 对象来迭代。例如,在 Python2.x 中,代码:

清单 3. 通过 iterable 对象来迭代

for i in range(1000): pass 

会导致生成一个 1000 个元素的 List,而代码:

for i in xrange(1000): pass

则不会生成一个 1000 个元素的 List,而是在每次迭代中返回下一个数值,内存空间占用很小。因为 xrange 不返回 List,而是返回一个 iterable 对象。

利用 iterable 我们可以把 fab 函数改写为一个支持 iterable 的 class,以下是第三个版本的 Fab:

清单 4. 第三个版本

class Fab(object):

    def __init__(self, max):
        self.max = max
        self.n, self.a, self.b = 0, 0, 1

    def __iter__(self):
        return self

    def next(self):
        if self.n < self.max:
            r = self.b
            self.a, self.b = self.b, self.a + self.b
            self.n = self.n + 1
            return r
        raise StopIteration()

Fab 类通过 next() 不断返回数列的下一个数,内存占用始终为常数:

>>> for n in Fab(5):
...     print n
...
1
1
2
3
5

然而,使用 class 改写的这个版本,代码远远没有第一版的 fab 函数来得简洁。如果我们想要保持第一版 fab 函数的简洁性,同时又要获得 iterable 的效果,yield 就派上用场了:

清单 5. 使用 yield 的第四版

def fab(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        # print b
        a, b = b, a + b
        n = n + 1

第四个版本的 fab 和第一版相比,仅仅把 print b 改为了 yield b,就在保持简洁性的同时获得了 iterable 的效果。

调用第四版的 fab 和第二版的 fab 完全一致:

>>> for n in fab(5):
...     print n
...
1
1
2
3
5

简单地讲,yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。

也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:

清单 6. 执行流程

>>> f = fab(5)
>>> f.next()
1
>>> f.next()
1
>>> f.next()
2
>>> f.next()
3
>>> f.next()
5
>>> f.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

当函数执行结束时,generator 自动抛出 StopIteration 异常,表示迭代完成。在 for 循环里,无需处理 StopIteration 异常,循环会正常结束。

我们可以得出以下结论:

一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。

yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

如何判断一个函数是否是一个特殊的 generator 函数?可以利用 isgeneratorfunction 判断:

清单 7. 使用 isgeneratorfunction 判断

>>> from inspect import isgeneratorfunction
>>> isgeneratorfunction(fab)
True

要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别:

清单 8. 类的定义和类的实例

>>> import types
>>> isinstance(fab, types.GeneratorType)
False
>>> isinstance(fab(5), types.GeneratorType)
True

fab 是无法迭代的,而 fab(5) 是可迭代的:

>>> from collections import Iterable
>>> isinstance(fab, Iterable)
False
>>> isinstance(fab(5), Iterable)
True

每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响:

>>> f1 = fab(3)
>>> f2 = fab(5)
>>> print 'f1:', f1.next()
f1: 1
>>> print 'f2:', f2.next()
f2: 1
>>> print 'f1:', f1.next()
f1: 1
>>> print 'f2:', f2.next()
f2: 1
>>> print 'f1:', f1.next()
f1: 2
>>> print 'f2:', f2.next()
f2: 2
>>> print 'f2:', f2.next()
f2: 3
>>> print 'f2:', f2.next()
f2: 5

另一个例子

另一个 yield 的例子来源于文件读取。如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:

清单 9. 另一个 yield 的例子

def read_file(fpath):
    BLOCK_SIZE = 1024
    with open(fpath, 'rb') as f:
        while True:
            block = f.read(BLOCK_SIZE)
            if block:
                yield block
            else:
                return

以上仅仅简单介绍了 yield 的基本概念和用法,yield 在 Python 3 中还有更强大的用法,我们会在后续文章中讨论。

注:本文的代码均在 Python 2.7 中调试通过

关于

<think>我们正在讨论Python中的yield关键字,根据用户的需求,他们想知道yield的使用方法和原理。根据提供的引用,我们可以总结如下: 1. yield用于创建生成器函数,这样的函数返回一个生成器对象。 2. 生成器函数在执行时,每次遇到yield会暂停并返回一个值,下次执行时从暂停处继续。 3. 生成器可以用于迭代,节省内存,因为它是按需生成值,而不是一次性生成所有值。 我们将从使用方法和原理两方面进行说明。 ### 使用方法 在函数中使用yield关键字,这个函数就变成了生成器函数。调用生成器函数时,它不会执行函数体,而是返回一个生成器对象。然后,可以使用next()函数或for循环来迭代生成器对象,每次迭代都会执行函数直到遇到yield,返回yield后面的值,并暂停。下次迭代时,从上次暂停的地方继续执行,直到再次遇到yield。如果函数执行结束(遇到return或执行到末尾),生成器会抛出StopIteration异常。 示例(来自引用[2]): ```python def createGenerator(): mylist = range(3) for i in mylist: yield i*i mygenerator = createGenerator() # 创建一个生成器对象 print(mygenerator) # 输出:<generator object createGenerator at 0xb7555c34> # 使用for循环迭代 for i in mygenerator: print(i) # 输出: # 0 # 1 # 4 ``` ### 原理 生成器函数内部维护一个状态,记录每次执行的位置。当函数被挂起时,它会保存当前的执行上下文(包括局部变量、指令指针等),以便在下次调用时恢复。这种机制称为“协程”。 引用[3]提到:当我们调用生成器函数时,函数体中的代码并没有运行,而是返回一个生成器对象。然后每次使用next()时,代码会从上次退出的地方继续运行。 生成器内部实现了迭代器协议,即包含`__iter__`和`__next__`方法。当我们使用for循环时,实际上是调用了生成器的`__next__`方法,直到遇到StopIteration。 引用[4]展示了一个使用生成器的例子,其中生成器用于树或图的遍历,可以按需产生节点,避免一次性返回所有节点占用过多内存。 ### 总结 yield的主要用途是创建生成器,实现惰性求值(按需生成),这在处理大数据或无限序列时非常有用。它通过保存和恢复执行状态来实现暂停和继续。 §§相关问题§§ 1. 生成器与普通函数的主要区别是什么? 2. 生成器在哪些场景下特别有用? 3. 如何捕获生成器的StopIteration异常? </think> ### Python yield 的使用与原理 #### 一、基本概念 在 Python 中,`yield` 关键字用于定义**生成器函数**(generator function)。这类函数与普通函数的区别在于: 1. 调用生成器函数时不会立即执行,而是返回一个生成器对象(`generator object`) 2. 执行到 `yield` 语句时会暂停,将 `yield` 后的值返回给调用者 3. 下次调用时从暂停处继续执行(保留所有局部变量状态) 示例(引用[2]): ```python def createGenerator(): mylist = range(3) for i in mylist: yield i*i # 每次迭代返回一个值并暂停 gen = createGenerator() # 返回生成器对象 <generator object> for val in gen: print(val) # 输出: 0 → 1 → 4 ``` #### 二、核心原理 1. **状态保持机制** 生成器内部维护执行状态(指令指针/局部变量),每次 `yield` 暂停时保存当前状态,下次调用通过 `__next__()` 恢复执行[^3]。 2. **惰性求值** 数据按需生成,避免一次性加载所有数据到内存,适合处理大型数据集(如文件流、无限序列)[^1]。 3. **协程实现基础** `yield` 实现了简单的协程,允许在单线程内实现任务切换(配合 `send()` 方法可实现双向数据传输)。 #### 三、典型使用场景 1. **大数据分批处理** ```python def read_large_file(file_path): with open(file_path) as f: while chunk := f.read(1024): # 每次读取1KB yield chunk # 分批返回数据,避免内存溢出 ``` 2. **无限序列生成** ```python def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b # 无限生成斐波那契数列 ``` 3. **状态机实现** ```python def state_machine(): state = "START" while True: if state == "START": # ...初始化操作 state = yield "READY" elif state == "RUNNING": # ...运行业务逻辑 state = yield "PROCESSING" ``` #### 四、高级用法 1. **send() 方法交互** ```python def interactive_gen(): val = yield "START" while True: val = yield f"Received: {val}" g = interactive_gen() print(next(g)) # 输出: START print(g.send("Hi")) # 输出: Received: Hi ``` 2. **yield from 委托** ```python def sub_generator(): yield from range(3) # 委托给另一个生成器 for i in sub_generator(): print(i) # 输出: 0 → 1 → 2 ``` #### 五、注意事项 1. 生成器只能遍历一次,再次遍历需重新创建 2. 可通过 `return` 结束生成器(触发 `StopIteration`) 3. 在异步编程中常用 `async/await` 替代复杂 `yield` 逻辑 > 关键原理总结:`yield` 将函数转换为可暂停的**状态机**,通过保存/恢复执行上下文实现高效迭代[^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值