Python 中 yield 语句的管理

Beginner

This tutorial is from open-source community. Access the source code

简介

在这个实验中,你将学习如何管理 Python 中 yield 语句的行为。你将了解如何有效地处理与这些语句相关的操作和行为。

此外,你还将学习生成器的生命周期以及生成器中的异常处理。在学习过程中,你将对 follow.pycofollow.py 文件进行修改。

理解生成器的生命周期和关闭操作

在这一步中,我们将探索 Python 生成器的生命周期,并学习如何正确地关闭它们。Python 中的生成器是一种特殊类型的迭代器,它允许你动态生成一系列值,而不是一次性计算所有值并将它们存储在内存中。在处理大型数据集或无限序列时,这非常有用。

什么是 follow() 生成器?

让我们首先查看项目目录中的 follow.py 文件。这个文件包含一个名为 follow() 的生成器函数。生成器函数的定义与普通函数类似,但它使用 yield 而不是 return 关键字。当调用生成器函数时,它会返回一个生成器对象,你可以对该对象进行迭代以获取它产生的值。

follow() 生成器函数会持续从文件中读取行,并在读取到每一行时将其产生出来。这类似于 Unix 的 tail -f 命令,该命令会持续监控文件以获取新行。

在 WebIDE 编辑器中打开 follow.py 文件:

import os
import time

def follow(filename):
    with open(filename,'r') as f:
        f.seek(0,os.SEEK_END)
        while True:
            line = f.readline()
            if line == '':
                time.sleep(0.1)    ## Sleep briefly to avoid busy wait
                continue
            yield line

在这段代码中,with open(filename, 'r') as f 语句以只读模式打开文件,并确保在代码块结束时文件被正确关闭。f.seek(0, os.SEEK_END) 行将文件指针移动到文件末尾,这样生成器就从文件末尾开始读取。while True 循环会持续从文件中读取行。如果行为空,说明还没有新行,程序会休眠 0.1 秒以避免忙等待,然后继续下一次迭代。如果行不为空,则将其产生出来。

这个生成器在一个无限循环中运行,这就引出了一个重要的问题:当我们停止使用生成器或想要提前终止它时会发生什么?

修改生成器以处理关闭操作

我们需要修改 follow.py 中的 follow() 函数,以处理生成器被正确关闭的情况。为此,我们将添加一个 try-except 块来捕获 GeneratorExit 异常。当生成器通过垃圾回收或调用 close() 方法被关闭时,会引发 GeneratorExit 异常。

import os
import time

def follow(filename):
    try:
        with open(filename,'r') as f:
            f.seek(0,os.SEEK_END)
            while True:
                line = f.readline()
                if line == '':
                    time.sleep(0.1)    ## Sleep briefly to avoid busy wait
                    continue
                yield line
    except GeneratorExit:
        print('Following Done')

在这段修改后的代码中,try 块包含了生成器的主要逻辑。如果引发了 GeneratorExit 异常,except 块会捕获它并打印消息 'Following Done'。这是在生成器关闭时执行清理操作的一种简单方法。

完成这些更改后保存文件。

对生成器关闭操作进行实验

现在,让我们进行一些实验,看看生成器在被垃圾回收或显式关闭时的行为。

打开一个终端并运行 Python 解释器:

cd ~/project
python3

实验 1:运行中的生成器的垃圾回收

>>> from follow import follow
>>> ## Experiment: Garbage collection of a running generator
>>> f = follow('stocklog.csv')
>>> next(f)
'"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314\n'
>>> del f  ## Delete the generator object
Following Done  ## This message appears because of our GeneratorExit handler

在这个实验中,我们首先从 follow.py 文件中导入 follow 函数。然后,我们通过调用 follow('stocklog.csv') 创建一个生成器对象 f。我们使用 next() 函数从生成器中获取下一行。最后,我们使用 del 语句删除生成器对象。当生成器对象被删除时,它会自动关闭,这会触发我们的 GeneratorExit 异常处理程序,并打印消息 'Following Done'。

实验 2:显式关闭生成器

>>> f = follow('stocklog.csv')
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         f.close()  ## Explicitly close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
Following Done
>>> for line in f:
...     print(line, end='')  ## No output: generator is closed
...

在这个实验中,我们创建一个新的生成器对象 f,并使用 for 循环对其进行迭代。在循环内部,我们打印每一行,并检查该行是否包含字符串 'IBM'。如果包含,我们调用生成器的 close() 方法来显式关闭它。当生成器被关闭时,会引发 GeneratorExit 异常,我们的异常处理程序会打印消息 'Following Done'。生成器关闭后,如果我们再次尝试对其进行迭代,将不会有输出,因为生成器已经不再活跃。

实验 3:跳出并恢复生成器的迭代

>>> f = follow('stocklog.csv')
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         break  ## Break out of the loop, but don't close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
>>> ## Resume iteration - the generator is still active
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         break
...
"CAT",78.36,"6/11/2007","09:37.19",-0.16,78.32,78.36,77.99,237714
"VZ",42.99,"6/11/2007","09:37.20",-0.08,42.95,42.99,42.78,268459
"IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859
>>> del f  ## Clean up
Following Done

在这个实验中,我们创建一个生成器对象 f,并使用 for 循环对其进行迭代。在循环内部,我们打印每一行,并检查该行是否包含字符串 'IBM'。如果包含,我们使用 break 语句跳出循环。跳出循环并不会关闭生成器,因此生成器仍然活跃。然后,我们可以通过对同一个生成器对象启动一个新的 for 循环来恢复迭代。最后,我们删除生成器对象以进行清理,这会触发 GeneratorExit 异常处理程序。

关键要点

  1. 当生成器被关闭(通过垃圾回收或调用 close() 方法)时,会在生成器内部引发 GeneratorExit 异常。
  2. 你可以捕获这个异常,以便在生成器关闭时执行清理操作。
  3. 使用 break 语句跳出生成器的迭代并不会关闭生成器,因此可以在稍后恢复迭代。

通过输入 exit() 或按下 Ctrl+D 退出 Python 解释器。

处理生成器中的异常

在这一步中,你将学习如何处理生成器和协程中的异常。不过首先,让我们了解一下什么是异常。异常是程序执行过程中发生的事件,它会打乱程序指令的正常执行流程。在 Python 中,你可以使用 throw() 方法来处理生成器和协程中的异常。

理解协程

协程是一种特殊类型的生成器。与主要用于产生值的常规生成器不同,协程既可以消耗值(使用 send() 方法),也可以产生值。cofollow.py 文件中有一个简单的协程实现。

让我们在 WebIDE 编辑器中打开 cofollow.py 文件。以下是其中的代码:

def consumer(func):
    def start(*args,**kwargs):
        c = func(*args,**kwargs)
        next(c)
        return c
    return start

@consumer
def printer():
    while True:
        item = yield
        print(item)

现在,让我们来分析一下这段代码。consumer 是一个装饰器。装饰器是一种函数,它接受另一个函数作为参数,为其添加一些功能,然后返回修改后的函数。在这种情况下,consumer 装饰器会自动将生成器移动到其第一个 yield 语句处。这很重要,因为它使生成器准备好接收值。

printer() 协程使用 @consumer 装饰器进行定义。在 printer() 函数内部,有一个无限的 while 循环。item = yield 语句是关键所在。它会暂停协程的执行,并等待接收一个值。当有值发送给协程时,它会恢复执行并打印接收到的值。

为协程添加异常处理

现在,你要修改 printer() 协程以处理异常。你可以像下面这样更新 cofollow.py 中的 printer() 函数:

@consumer
def printer():
    while True:
        try:
            item = yield
            print(item)
        except Exception as e:
            print('ERROR: %r' % e)

try 块包含可能引发异常的代码。在这个例子中,就是接收并打印值的代码。如果 try 块中发生异常,执行流程会跳转到 except 块。except 块会捕获异常并打印错误消息。完成这些修改后,保存文件。

对协程中的异常处理进行实验

让我们开始实验如何向协程中抛出异常。打开一个终端,并使用以下命令运行 Python 解释器:

cd ~/project
python3

实验 1:基本的协程使用

>>> from cofollow import printer
>>> p = printer()
>>> p.send('hello')  ## Send a value to the coroutine
hello
>>> p.send(42)  ## Send another value
42

在这里,你首先从 cofollow 模块中导入 printer 协程。然后创建一个名为 pprinter 协程实例。你使用 send() 方法向协程发送值。如你所见,协程可以顺利处理你发送给它的值。

实验 2:向协程中抛出异常

>>> p.throw(ValueError('It failed'))  ## Throw an exception into the coroutine
ERROR: ValueError('It failed')

在这个实验中,你使用 throw() 方法向协程中注入一个 ValueError 异常。printer() 协程中的 try-except 块会捕获该异常并打印错误消息。这表明你的异常处理按预期工作。

实验 3:向协程中抛出实际的异常

>>> try:
...     int('n/a')  ## This will raise a ValueError
... except ValueError as e:
...     p.throw(e)  ## Throw the caught exception into the coroutine
...
ERROR: ValueError("invalid literal for int() with base 10: 'n/a'")

在这里,你首先尝试将字符串 'n/a' 转换为整数,这会引发一个 ValueError 异常。你捕获这个异常,然后使用 throw() 方法将其传递给协程。协程会捕获该异常并打印错误消息。

实验 4:验证协程继续运行

>>> p.send('still working')  ## The coroutine continues to run after handling exceptions
still working

在处理完异常后,你使用 send() 方法向协程发送另一个值。协程仍然处于活动状态,并且可以处理新的值。这表明你的协程即使在遇到错误后也能继续运行。

关键要点

  1. 生成器和协程可以在 yield 语句处处理异常。这意味着你可以捕获并处理协程在等待或处理值时发生的错误。
  2. throw() 方法允许你向生成器或协程中注入异常。这对于测试和处理协程外部发生的错误很有用。
  3. 在生成器中正确处理异常可以让你创建健壮、容错的生成器,即使发生错误也能继续运行。这会使你的代码更可靠,更易于维护。

要退出 Python 解释器,你可以输入 exit() 或按下 Ctrl+D

生成器管理的实际应用

在这一步中,我们将探索如何把所学的生成器管理和异常处理概念应用到实际场景中。理解这些实际应用将帮助你编写更健壮、高效的 Python 代码。

创建一个健壮的文件监控系统

让我们构建一个更可靠的文件监控系统。这个系统能够处理不同的情况,例如超时和用户的停止请求。

首先,打开 WebIDE 编辑器,创建一个名为 robust_follow.py 的新文件。你需要在这个文件中编写以下代码:

import os
import time
import signal

class TimeoutError(Exception):
    pass

def timeout_handler(signum, frame):
    raise TimeoutError("Operation timed out")

def follow(filename, timeout=None):
    """
    A generator that yields new lines in a file.
    With timeout handling and proper cleanup.
    """
    try:
        ## Set up timeout if specified
        if timeout:
            signal.signal(signal.SIGALRM, timeout_handler)
            signal.alarm(timeout)

        with open(filename, 'r') as f:
            f.seek(0, os.SEEK_END)
            while True:
                line = f.readline()
                if line == '':
                    ## No new data, wait briefly
                    time.sleep(0.1)
                    continue
                yield line
    except TimeoutError:
        print(f"Following timed out after {timeout} seconds")
    except GeneratorExit:
        print("Following stopped by request")
    finally:
        ## Clean up timeout alarm if it was set
        if timeout:
            signal.alarm(0)
        print("Follow generator cleanup complete")

在这段代码中,我们首先定义了一个自定义的 TimeoutError 类。timeout_handler 函数用于在超时发生时抛出这个错误。follow 函数是一个生成器,它读取文件并产生新的行。如果指定了超时时间,它会使用 signal 模块设置一个警报。如果文件中没有新数据,它会短暂等待后再尝试。try - except - finally 块用于处理不同的异常,并确保进行适当的清理。

编写完代码后,保存文件。

测试健壮的文件监控系统

现在,让我们测试改进后的文件监控系统。打开一个终端,使用以下命令运行 Python 解释器:

cd ~/project
python3

实验 1:基本使用

在 Python 解释器中,我们将测试 follow 生成器的基本功能。运行以下代码:

>>> from robust_follow import follow
>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
...     print(f"Line {i+1}: {line.strip()}")
...     if i >= 2:  ## Just read a few lines for the example
...         break
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Line 3: "HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169

在这里,我们从 robust_follow.py 文件中导入 follow 函数。然后创建一个生成器对象 f,用于监控 stocklog.csv 文件。我们使用 for 循环迭代生成器产生的行,并打印前 3 行。

实验 2:使用超时功能

让我们看看超时功能是如何工作的。在 Python 解释器中运行以下代码:

>>> ## Create a generator that will time out after 3 seconds
>>> f = follow('stocklog.csv', timeout=3)
>>> for line in f:
...     print(line.strip())
...     time.sleep(1)  ## Process each line slowly
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
Following timed out after 3 seconds
Follow generator cleanup complete

在这个实验中,我们创建了一个带有 3 秒超时时间的生成器。我们通过在每行处理之间休眠 1 秒来缓慢处理每行。大约 3 秒后,生成器抛出超时异常,finally 块中的清理代码会被执行。

实验 3:显式关闭

让我们测试生成器如何处理显式关闭。运行以下代码:

>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
...     print(f"Line {i+1}: {line.strip()}")
...     if i >= 1:
...         print("Explicitly closing the generator...")
...         f.close()
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Explicitly closing the generator...
Following stopped by request
Follow generator cleanup complete

在这里,我们创建一个生成器并开始迭代其行。处理 2 行后,我们使用 close 方法显式关闭生成器。然后生成器会处理 GeneratorExit 异常并执行必要的清理操作。

创建一个带有错误处理的数据处理管道

接下来,我们将使用协程创建一个简单的数据处理管道。这个管道能够在不同阶段处理错误。

打开 WebIDE 编辑器,创建一个名为 pipeline.py 的新文件。在这个文件中编写以下代码:

def consumer(func):
    def start(*args,**kwargs):
        c = func(*args,**kwargs)
        next(c)
        return c
    return start

@consumer
def grep(pattern, target):
    """Filter lines containing pattern and send to target"""
    try:
        while True:
            line = yield
            if pattern in line:
                target.send(line)
    except Exception as e:
        target.throw(e)

@consumer
def printer():
    """Print received items"""
    try:
        while True:
            item = yield
            print(f"PRINTER: {item}")
    except Exception as e:
        print(f"PRINTER ERROR: {repr(e)}")

def follow_and_process(filename, pattern):
    """Follow a file and process its contents"""
    import time
    import os

    output = printer()
    filter_pipe = grep(pattern, output)

    try:
        with open(filename, 'r') as f:
            f.seek(0, os.SEEK_END)
            while True:
                line = f.readline()
                if not line:
                    time.sleep(0.1)
                    continue
                filter_pipe.send(line)
    except KeyboardInterrupt:
        print("Processing stopped by user")
    finally:
        filter_pipe.close()
        output.close()

在这段代码中,consumer 装饰器用于初始化协程。grep 协程过滤包含特定模式的行,并将它们发送到另一个协程。printer 协程打印接收到的项。follow_and_process 函数读取文件,使用 grep 协程过滤其行,并使用 printer 协程打印匹配的行。它还处理 KeyboardInterrupt 异常,并确保进行适当的清理。

编写完代码后,保存文件。

测试数据处理管道

让我们测试数据处理管道。在终端中运行以下命令:

cd ~/project
python3 -c "from pipeline import follow_and_process; follow_and_process('stocklog.csv', 'IBM')"

你应该会看到类似以下的输出:

PRINTER: "IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550

PRINTER: "IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859

PRINTER: "IBM",102.95,"6/11/2007","09:39.44",-0.12,102.87,102.95,102.77,225350

这个输出表明管道正常工作,过滤并打印了包含 "IBM" 模式的行。

要停止这个进程,按 Ctrl+C。你应该会看到以下消息:

Processing stopped by user

关键要点

  1. 在生成器中进行适当的异常处理可以让你创建能够优雅处理错误的健壮系统。这意味着当出现问题时,你的程序不会意外崩溃。
  2. 你可以使用超时等技术来防止生成器无限运行。这有助于管理系统资源,并确保你的程序不会陷入无限循环。
  3. 生成器和协程可以形成强大的数据处理管道,在其中可以在适当的级别传播和处理错误。这使得构建复杂的数据处理系统变得更容易。
  4. 生成器中的 finally 块确保无论生成器如何终止,都会执行清理操作。这有助于维护程序的完整性,并防止资源泄漏。

总结

在本次实验中,你学习了在 Python 生成器和协程中管理 yield 语句的重要技术。你探索了生成器的生命周期管理,包括在关闭或垃圾回收期间处理 GeneratorExit 异常,以及控制迭代的中断和恢复。此外,你还学习了生成器中的异常处理,例如使用 throw() 方法,以及编写能够优雅处理异常的健壮生成器。

这些技术是构建健壮、可维护的 Python 应用程序的基础。它们在数据处理、异步操作和资源管理方面非常有用。通过正确管理生成器的生命周期并处理异常,你可以创建能够优雅处理错误并在资源不再需要时进行清理的弹性系统。