返回

Python yield 详解:原理、用法及进阶技巧

python

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处理。

进阶用法

  1. 生成器表达式: 类似于列表推导式, 但用圆括号:

    squares = (x*x for x in range(10))
    for square in squares:
        print(square)
    
  2. 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]
    
  3. 创建双向沟通的“协程”: 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 的更多妙用!