Python yield 详解:原理、用法及进阶技巧
2025-03-14 21:10:20
Python 中 yield
的妙用
你是不是也遇到过 Python 代码里蹦出个 yield
,看得一脸懵? 别慌,今天我们就来把 yield
掰开揉碎了好好说道说道。
yield
到底是个啥?
先看问题里的这段代码:
def _get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
调用它的代码长这样:
result, candidates = [], [self]
while candidates:
node = candidates.pop()
distance = node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
_get_child_candidates
这函数被调用时,会返回一个列表吗?还是返回单个元素?它会被反复调用吗?啥时候停下来呢? 要弄明白这些,关键就在于理解 yield
。
简单来说,yield
的作用就是把一个函数变成一个 生成器 (generator)。 生成器是啥? 它是一种特殊的 迭代器 (iterator)。 迭代器又是什么?别急,我们一步步来。
迭代器 (Iterator) 和 可迭代对象 (Iterable)
想象一下,你有一本花名册,你想挨个点名。 这本花名册,就是 可迭代对象 (iterable)。 你翻页、读名字这个动作,就由 迭代器 (iterator) 来完成。
在 Python 里, 很多东西都是可迭代的,比如列表、元组、字符串、字典:
my_list = [1, 2, 3]
for item in my_list:
print(item)
这里, my_list
就是可迭代对象。 for
循环在背后偷偷创建了一个迭代器,帮你遍历 my_list
里的每一个元素。
你可以用 iter()
函数手动创建一个迭代器:
my_list = [1, 2, 3]
my_iterator = iter(my_list)
print(next(my_iterator)) # 输出 1
print(next(my_iterator)) # 输出 2
print(next(my_iterator)) # 输出 3
print(next(my_iterator)) # 抛出 StopIteration 异常
每次调用 next()
,迭代器就返回下一个元素。 当没有元素可返回时, 它就抛出一个 StopIteration
异常。
生成器 (Generator): yield
的舞台
终于轮到生成器出场了! 生成器是一种特殊的迭代器,它的特点是: 按需生成值。
普通函数,像这样:
def get_numbers(n):
numbers = []
for i in range(n):
numbers.append(i)
return numbers
这个函数会 一次性 生成所有数字,然后把它们装在一个列表里返回。 如果 n
很大,这个列表会占用很多内存。
生成器版本的函数:
def get_numbers_generator(n):
for i in range(n):
yield i
这个函数,看起来没啥特别的,就是把 return
换成了 yield
。 但它的行为完全不同!
当你调用 get_numbers_generator(5)
,它并 不会 立即执行循环,也不会返回一个列表。 它返回的是一个 生成器对象。
my_generator = get_numbers_generator(5)
print(my_generator) # 输出 <generator object get_numbers_generator at 0x...>
只有当你开始迭代这个生成器对象时 (比如用 for
循环, 或者 next()
函数), 它才会 开始执行。
for number in my_generator:
print(number) # 依次输出 0, 1, 2, 3, 4
每次遇到 yield
,函数就暂停执行, 把 yield
后面的值返回。 下次迭代时,函数就从上次暂停的地方 继续 执行,直到遇到下一个 yield
,或者函数结束。
yield
工作原理:状态保存
yield
的神奇之处在于, 它能让函数 暂停 和 恢复 执行,并且 保存 函数的状态。
你可以把 yield
看作一个传送门。 每次遇到 yield
,函数就通过传送门把一个值送出来,然后自己进入休眠状态。 下次需要值的时候, 函数就从休眠中醒来,从传送门旁边继续执行。
这就好比你在玩游戏, yield
就是存档点。 每次遇到存档点,游戏进度就保存下来。 下次你可以从存档点继续玩,而不是从头开始。
回到最初的问题
现在,我们再来看 _get_child_candidates
函数:
def _get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
这个函数,根据条件,可能会 yield
左子节点,或者 yield
右子节点,或者两个都 yield
,或者一个都不 yield
。
每次 yield
一个子节点, 函数就暂停。 调用者通过迭代,拿到这个子节点, 进行处理。 如果还有子节点要 yield
, 函数就恢复执行,yield
下一个子节点。
candidates.extend(node._get_child_candidates(...))
这行代码, 把 _get_child_candidates
返回的生成器里的所有子节点,都添加到 candidates
列表里。
while candidates:
循环持续进行,直到 candidates
列表为空, 也就是所有节点及其子节点都被处理完毕。
yield
的好处
- 节省内存: 生成器按需生成值, 不会一次性把所有结果都加载到内存里, 特别适合处理大数据集。
- 延迟计算: 只有在需要的时候才计算值, 提高效率。
- 代码更简洁: 可以用更简洁的代码实现复杂的迭代逻辑。
- 流式处理: 可以和管道组合实现数据的流式处理,特别适合日志分析和ETL处理。
进阶用法
-
生成器表达式: 类似于列表推导式, 但用圆括号:
squares = (x*x for x in range(10)) for square in squares: print(square)
-
yield from
: 用于从另一个生成器yield
值 (Python 3.3+):def chain(*iterables): for it in iterables: yield from it s = 'ABC' t = tuple(range(3)) print(list(chain(s, t))) # 输出 ['A', 'B', 'C', 0, 1, 2]
-
创建双向沟通的“协程”: send()方法与yield的结合。
```python
def my_coroutine():
while True:
received = yield
print('Received:', received)
coro = my_coroutine()
next(coro) # 启动协程
coro.send('Hello') # 输出 Received: Hello
coro.send('World') # 输出 Received: World
```
yield
是 Python 里非常强大且有用的工具,理解了它,能让你的代码更优雅、更高效。 多多练习, 你会发现 yield
的更多妙用!