学习委托生成器

Beginner

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

简介

在这个实验中,你将学习如何使用 yield from 语句在 Python 中委托生成器。这个特性是在 Python 3.3 中引入的,它简化了依赖生成器和协程的代码。

生成器是一种特殊的函数,它可以暂停和恢复执行,并在多次调用之间保留其状态。yield from 语句提供了一种优雅的方式将控制权委托给另一个生成器,从而提高代码的可读性和可维护性。

目标:

  • 理解 yield from 语句的用途
  • 学习如何使用 yield from 委托给其他生成器
  • 运用这些知识简化基于协程的代码
  • 理解与现代 async/await 语法的联系

你将使用的文件:

  • cofollow.py - 包含协程实用函数
  • server.py - 包含一个简单的网络服务器实现

理解 yield from 语句

在这一步中,我们将探索 Python 中的 yield from 语句。这个语句在处理生成器时是一个强大的工具,它简化了将操作委托给其他生成器的过程。在这一步结束时,你将理解 yield from 是什么、它是如何工作的,以及它如何处理不同生成器之间的值传递。

什么是 yield from

yield from 语句是在 Python 3.3 中引入的。它的主要目的是简化将操作委托给子生成器(subgenerator)的过程。子生成器就是主生成器可以委托工作的另一个生成器。

通常,当你希望一个生成器从另一个生成器中产出值时,你必须使用一个循环。例如,不使用 yield from 时,你会编写如下代码:

def delegating_generator():
    for value in subgenerator():
        yield value

在这段代码中,delegating_generator 使用一个 for 循环来迭代 subgenerator 产生的值,然后逐个产出这些值。

然而,使用 yield from 语句后,代码变得简单得多:

def delegating_generator():
    yield from subgenerator()

这一行代码实现了与前一个示例中的循环相同的结果。但 yield from 不仅仅是一个捷径。它还管理调用者和子生成器之间的双向通信。这意味着发送给委托生成器的任何值都会直接传递给子生成器。

基本示例

让我们创建一个简单的示例,看看 yield from 是如何实际工作的。

  1. 首先,你需要在编辑器中打开 cofollow.py 文件。为此,我们将使用 cd 命令导航到正确的目录。在终端中运行以下命令:
cd /home/labex/project
  1. 接下来,我们将在 cofollow.py 文件中添加两个函数。subgen 函数是一个简单的生成器,它产出从 0 到 4 的数字。main_gen 函数使用 yield from 将这些数字的生成委托给 subgen,然后产出字符串 'Done'。将以下代码添加到 cofollow.py 文件的末尾:
def subgen():
    for i in range(5):
        yield i

def main_gen():
    yield from subgen()
    yield 'Done'
  1. 现在,让我们测试这些函数。打开一个 Python 交互式环境并运行以下代码:
from cofollow import subgen, main_gen

## Test subgen directly
for x in subgen():
    print(x)

## Test main_gen that delegates to subgen
for x in main_gen():
    print(x)

当你运行这段代码时,你应该会看到以下输出:

0
1
2
3
4

0
1
2
3
4
Done

这个输出表明 yield from 允许 main_gensubgen 生成的所有值直接传递给调用者。

使用 yield from 进行值传递

yield from 最强大的特性之一是它能够处理双向的值传递。让我们创建一个更复杂的示例来演示这一点。

  1. 将以下函数添加到 cofollow.py 文件中:
def accumulator():
    total = 0
    while True:
        value = yield total
        if value is None:
            break
        total += value

def caller():
    acc = accumulator()
    yield from acc
    yield 'Total accumulated'

accumulator 函数是一个协程,它跟踪一个累加的总数。它产出当前的总数,然后等待接收一个新的值。如果接收到 None,它就会停止循环。caller 函数创建一个 accumulator 的实例,并使用 yield from 将所有的发送和接收操作委托给它。

  1. 在 Python 交互式环境中测试这些函数:
from cofollow import caller

c = caller()
print(next(c))  ## Start the coroutine
print(c.send(1))  ## Send value 1, get accumulated value
print(c.send(2))  ## Send value 2, get accumulated value
print(c.send(3))  ## Send value 3, get accumulated value
print(c.send(None))  ## Send None to exit the accumulator

当你运行这段代码时,你应该会看到以下输出:

0
1
3
6
'Total accumulated'

这个输出表明 yield from 会将所有的发送和接收操作完全委托给子生成器,直到子生成器耗尽为止。

现在你已经理解了 yield from 的基础知识,我们将在下一步中继续探讨更实际的应用。

在协程中使用 yield from

在这一步中,我们将探索如何在协程中使用 yield from 语句,以实现更实际的应用。协程是 Python 中一个强大的概念,了解如何将 yield from 与协程结合使用可以极大地简化你的代码。

协程与消息传递

协程是一种特殊的函数,它可以通过 yield 语句接收值。它们在数据处理和事件处理等任务中非常有用。在 cofollow.py 文件中,有一个 consumer 装饰器。这个装饰器可以帮助设置协程,自动将协程推进到第一个 yield 点。这意味着你不必手动启动协程,装饰器会帮你完成这个操作。

让我们创建一个协程,它可以接收值并验证其类型。你可以按照以下步骤操作:

  1. 首先,在编辑器中打开 cofollow.py 文件。你可以在终端中使用以下命令导航到正确的目录:
cd /home/labex/project
  1. 接下来,在 cofollow.py 文件的末尾添加以下 receive 函数。这个函数是一个协程,它将接收一条消息并验证其类型。
def receive(expected_type):
    """
    A coroutine that receives a message and validates its type.
    Returns the received message if it matches the expected type.
    """
    msg = yield
    assert isinstance(msg, expected_type), f'Expected type {expected_type}'
    return msg

这个函数的功能如下:

  • 它使用不带表达式的 yield 来接收一个值。当协程接收到一个值时,这个 yield 语句会捕获它。
  • 它使用 isinstance 函数检查接收到的值是否为预期的类型。如果类型不匹配,它会引发一个 AssertionError
  • 如果类型检查通过,它会返回该值。
  1. 现在,让我们创建一个协程,它使用 yield from 调用我们的 receive 函数。这个新的协程将只接收并打印整数。
@consumer
def print_ints():
    """
    A coroutine that receives and prints integers only.
    Uses yield from to delegate to the receive coroutine.
    """
    while True:
        val = yield from receive(int)
        print('Got:', val)
  1. 为了测试这个协程,打开一个 Python 交互式环境并运行以下代码:
from cofollow import print_ints

p = print_ints()
p.send(42)
p.send(13)
try:
    p.send('13')  ## This should raise an AssertionError
except AssertionError as e:
    print(f"Error: {e}")

你应该会看到以下输出:

Got: 42
Got: 13
Error: Expected type <class 'int'>

理解 yield from 在协程中的工作原理

当我们在 print_ints 协程中使用 yield from receive(int) 时,会发生以下步骤:

  1. 控制权被委托给 receive 协程。这意味着 print_ints 协程暂停,receive 协程开始执行。
  2. receive 协程使用 yield 来接收一个值。它会等待一个值被发送给它。
  3. 当一个值被发送给 print_ints 时,实际上是由 receive 接收的。yield from 语句负责将值从 print_ints 传递给 receive
  4. receive 协程验证接收到的值的类型。如果类型正确,它会返回该值。
  5. 返回的值成为 print_ints 协程中 yield from 表达式的结果。这意味着 print_ints 中的 val 变量会被赋值为 receive 返回的值。

使用 yield from 使代码比直接处理 yieldreceive 更具可读性。它抽象掉了协程之间传递值的复杂性。

创建更高级的类型检查协程

让我们扩展我们的实用函数,以处理更复杂的类型验证。你可以按照以下步骤操作:

  1. cofollow.py 文件中添加以下函数:
def receive_dict():
    """Receive and validate a dictionary"""
    result = yield from receive(dict)
    return result

def receive_str():
    """Receive and validate a string"""
    result = yield from receive(str)
    return result

@consumer
def process_data():
    """Process different types of data using the receive utilities"""
    while True:
        print("Waiting for a string...")
        name = yield from receive_str()
        print(f"Got string: {name}")

        print("Waiting for a dictionary...")
        data = yield from receive_dict()
        print(f"Got dictionary with {len(data)} items: {data}")

        print("Processing complete for this round.")
  1. 为了测试新的协程,打开一个 Python 交互式环境并运行以下代码:
from cofollow import process_data

proc = process_data()
proc.send("John Doe")
proc.send({"age": 30, "city": "New York"})
proc.send("Jane Smith")
try:
    proc.send(123)  ## This should raise an AssertionError
except AssertionError as e:
    print(f"Error: {e}")

你应该会看到如下输出:

Waiting for a string...
Got string: John Doe
Waiting for a dictionary...
Got dictionary with 2 items: {'age': 30, 'city': 'New York'}
Processing complete for this round.
Waiting for a string...
Got string: Jane Smith
Waiting for a dictionary...
Error: Expected type <class 'dict'>

yield from 语句使代码更简洁、更易读。它让我们能够专注于程序的高级逻辑,而不必陷入协程之间消息传递的细节中。

使用生成器包装套接字

在这一步中,你将学习如何使用生成器来包装套接字操作。这是一个非常重要的概念,尤其在异步编程中。异步编程允许你的程序同时处理多个任务,而无需等待一个任务完成后再开始另一个任务。使用生成器包装套接字操作可以使你的代码更高效且更易于管理。

理解问题

server.py 文件包含一个使用生成器实现的简单网络服务器。让我们看一下当前的代码。这段代码是服务器的基础,在进行任何更改之前,理解它至关重要。

def tcp_server(address, handler):
    sock = socket(AF_INET, SOCK_STREAM)
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        yield 'recv', sock
        client, addr = sock.accept()
        tasks.append(handler(client, addr))

def echo_handler(client, address):
    print('Connection from', address)
    while True:
        yield 'recv', client
        data = client.recv(1000)
        if not data:
            break
        yield 'send', client
        client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

在这段代码中,我们使用了 yield 关键字。yield 关键字在 Python 中用于创建生成器。生成器是一种特殊类型的迭代器,它允许你暂停和恢复函数的执行。在这里,yield 用于指示服务器何时准备好接受连接,或者客户端处理程序何时准备好接收或发送数据。然而,手动的 yield 语句将事件循环的内部工作机制暴露给了用户。这意味着用户必须了解事件循环的工作原理,这会使代码更难理解和维护。

创建 GenSocket 类

让我们创建一个 GenSocket 类,用生成器来包装套接字操作。这将使我们的代码更简洁、更易读。通过将套接字操作封装在一个类中,我们可以向用户隐藏事件循环的细节,专注于服务器的高级逻辑。

  1. 在编辑器中打开 server.py 文件:
cd /home/labex/project

这个命令将当前目录更改为包含 server.py 文件的项目目录。进入正确的目录后,你可以使用你喜欢的文本编辑器打开该文件。

  1. 在文件末尾、现有函数之前添加以下 GenSocket 类:
class GenSocket:
    """
    A generator-based wrapper for socket operations.
    """
    def __init__(self, sock):
        self.sock = sock

    def accept(self):
        """Accept a connection and return a new GenSocket"""
        yield 'recv', self.sock
        client, addr = self.sock.accept()
        return GenSocket(client), addr

    def recv(self, maxsize):
        """Receive data from the socket"""
        yield 'recv', self.sock
        return self.sock.recv(maxsize)

    def send(self, data):
        """Send data to the socket"""
        yield 'send', self.sock
        return self.sock.send(data)

    def __getattr__(self, name):
        """Forward any other attributes to the underlying socket"""
        return getattr(self.sock, name)

这个 GenSocket 类作为套接字操作的包装器。__init__ 方法使用一个套接字对象初始化该类。acceptrecvsend 方法执行相应的套接字操作,并使用 yield 来指示操作何时准备好。__getattr__ 方法允许该类将任何其他属性转发给底层的套接字对象。

  1. 现在,修改 tcp_serverecho_handler 函数以使用 GenSocket 类:
def tcp_server(address, handler):
    sock = GenSocket(socket(AF_INET, SOCK_STREAM))
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        client, addr = yield from sock.accept()
        tasks.append(handler(client, addr))

def echo_handler(client, address):
    print('Connection from', address)
    while True:
        data = yield from client.recv(1000)
        if not data:
            break
        yield from client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

注意,显式的 yield 'recv', sockyield 'send', client 语句已被更简洁的 yield from 表达式所取代。yield from 关键字用于将执行委托给另一个生成器。这使得代码更易读,并向用户隐藏了事件循环的细节。现在,代码看起来更像普通的函数调用,用户不必担心事件循环的内部工作机制。

  1. 让我们添加一个简单的测试函数,以演示如何使用我们的服务器:
def run_server():
    """Start the server on port 25000"""
    tasks.append(tcp_server(('localhost', 25000), echo_handler))
    try:
        event_loop()
    except KeyboardInterrupt:
        print("Server stopped")

if __name__ == '__main__':
    print("Starting echo server on port 25000...")
    print("Press Ctrl+C to stop")
    run_server()

这段代码更易读且更易于维护。GenSocket 类封装了 yield 逻辑,使服务器代码能够专注于高级流程,而不是事件循环的细节。run_server 函数在端口 25000 上启动服务器,并处理 KeyboardInterrupt 异常,允许用户通过按 Ctrl+C 停止服务器。

理解优点

yield from 方法有几个优点:

  1. 代码更简洁:套接字操作看起来更像普通的函数调用。这使得代码更易于阅读和理解,尤其对于初学者来说。
  2. 抽象化:事件循环的细节对用户隐藏。用户无需了解事件循环的工作原理即可使用服务器代码。
  3. 可读性强:代码更能表达它在做什么,而不是如何做。这使得代码更具自解释性,更易于维护。
  4. 可维护性高:对事件循环的更改不需要更改服务器代码。这意味着如果你将来需要修改事件循环,可以在不影响服务器代码的情况下进行。

这种模式是迈向现代 async/await 语法的垫脚石,我们将在下一步中探索。async/await 语法是在 Python 中编写异步代码的更高级、更简洁的方式,理解 yield from 模式将帮助你更轻松地过渡到它。

从生成器到 async/await

在这最后一步中,我们将探索 Python 中的 yield from 模式是如何演变成现代的 async/await 语法的。理解这一演变过程至关重要,因为它能帮助你看清生成器与异步编程之间的联系。异步编程允许你的程序在不等待每个任务完成的情况下处理多个任务,这在网络编程和其他 I/O 密集型操作中尤为有用。

生成器与 async/await 的联系

Python 3.5 引入的 async/await 语法是建立在生成器和 yield from 功能之上的。实际上,async 函数是使用生成器实现的。这意味着你所学的关于生成器的概念与 async/await 的工作原理直接相关。

要从使用生成器过渡到 async/await 语法,需要遵循以下步骤:

  1. 使用 types 模块中的 @coroutine 装饰器。这个装饰器有助于将基于生成器的函数转换为可与 async/await 一起使用的形式。
  2. 将使用 yield from 的函数转换为使用 asyncawait。这会使代码更易读,并更好地体现操作的异步性质。
  3. 更新事件循环以处理原生协程。事件循环负责调度和运行异步任务。

更新 GenSocket

现在,让我们修改 GenSocket 类,使其能与 @coroutine 装饰器一起使用。这样,我们的类就可以在 async/await 上下文中使用了。

  1. 在编辑器中打开 server.py 文件。你可以在终端中运行以下命令来实现:
cd /home/labex/project
  1. server.py 文件的顶部,添加对 coroutine 的导入。要使用 @coroutine 装饰器,这个导入是必需的。
from types import coroutine
  1. 更新 GenSocket 类以使用 @coroutine 装饰器。这个装饰器将我们基于生成器的方法转换为可等待的协程,这意味着它们可以与 await 关键字一起使用。
class GenSocket:
    """
    A generator-based wrapper for socket operations
    that works with async/await.
    """
    def __init__(self, sock):
        self.sock = sock

    @coroutine
    def accept(self):
        """Accept a connection and return a new GenSocket"""
        yield 'recv', self.sock
        client, addr = self.sock.accept()
        return GenSocket(client), addr

    @coroutine
    def recv(self, maxsize):
        """Receive data from the socket"""
        yield 'recv', self.sock
        return self.sock.recv(maxsize)

    @coroutine
    def send(self, data):
        """Send data to the socket"""
        yield 'send', self.sock
        return self.sock.send(data)

    def __getattr__(self, name):
        """Forward any other attributes to the underlying socket"""
        return getattr(self.sock, name)

转换为 async/await 语法

接下来,让我们将服务器代码转换为使用 async/await 语法。这将使代码更易读,并清晰地体现操作的异步性质。

async def tcp_server(address, handler):
    """
    An asynchronous TCP server using async/await.
    """
    sock = GenSocket(socket(AF_INET, SOCK_STREAM))
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        client, addr = await sock.accept()
        tasks.append(handler(client, addr))

async def echo_handler(client, address):
    """
    An asynchronous handler for echo clients.
    """
    print('Connection from', address)
    while True:
        data = await client.recv(1000)
        if not data:
            break
        await client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

注意,yield from 已被 await 取代,并且函数现在使用 async def 而不是 def 来定义。这一改变使代码更直观、更易理解。

理解转换过程

从使用 yield from 的生成器过渡到 async/await 语法,不仅仅是简单的语法更改。它代表了我们对异步编程思考方式的转变。

  1. 使用 yield from 的生成器

    • 使用带有 yield from 的生成器时,你需要显式地让出控制权,以表明某个任务已准备好。这意味着你必须手动管理任务何时可以继续执行。
    • 你还需要手动管理任务的调度。这可能很复杂,尤其是在大型程序中。
    • 重点在于控制流的机制,这可能会使代码更难阅读和维护。
  2. async/await 语法

    • 使用 async/await 语法时,控制权会在 await 点隐式让出。这使代码更简洁,因为你不必担心显式地让出控制权。
    • 事件循环会负责任务的调度,因此你不必手动管理。
    • 重点在于程序的逻辑流程,这使代码更易读、更易维护。

这种转变使得异步代码更易读、更易维护,这对于像网络服务器这样的复杂应用尤为重要。

现代异步编程

在现代 Python 中,我们通常使用 asyncio 模块进行异步编程,而不是自定义事件循环。asyncio 模块为许多有用的功能提供了内置支持:

  • 并发运行多个协程。这允许你的程序同时处理多个任务。
  • 管理网络 I/O。它简化了通过网络发送和接收数据的过程。
  • 同步原语。这些有助于你在并发环境中管理对共享资源的访问。
  • 任务调度和取消。你可以轻松地安排任务在特定时间运行,并在需要时取消它们。

以下是使用 asyncio 实现的服务器示例:

import asyncio

async def handle_client(reader, writer):
    addr = writer.get_extra_info('peername')
    print(f'Connection from {addr}')

    while True:
        data = await reader.read(1000)
        if not data:
            break

        writer.write(b'GOT:' + data)
        await writer.drain()

    print('Connection closed')
    writer.close()
    await writer.wait_closed()

async def main():
    server = await asyncio.start_server(
        handle_client, 'localhost', 25000
    )

    addr = server.sockets[0].getsockname()
    print(f'Serving on {addr}')

    async with server:
        await server.serve_forever()

if __name__ == '__main__':
    asyncio.run(main())

这段代码实现了与基于生成器的服务器相同的功能,但使用了标准的 asyncio 库,该库更健壮且功能更丰富。

总结

在这个实验中,你学习了几个重要的概念:

  1. yield from 语句以及它如何委托给另一个生成器。这是理解生成器工作原理的基本概念。
  2. 如何在协程中使用 yield from 进行消息传递。这允许你在异步程序的不同部分之间进行通信。
  3. 使用生成器包装套接字操作,以使代码更简洁。这使与网络相关的代码更有条理、更易理解。
  4. 从生成器到现代 async/await 语法的过渡。理解这一过渡将帮助你在 Python 中编写更易读、更易维护的异步代码,无论你是直接使用生成器还是现代的 async/await 语法。

总结

在这个实验中,你学习了 Python 中委托生成器的概念,重点了解了 yield from 语句及其各种应用。你探索了如何使用 yield from 委托给另一个生成器,这简化了代码并提高了可读性。你还学习了如何使用 yield from 创建协程来接收和验证消息,以及如何使用生成器包装套接字操作,以使网络代码更简洁。

这些概念对于理解 Python 中的异步编程至关重要。从生成器到现代 async/await 语法的转变,代表了处理异步操作的重大进步。为了进一步探索这些概念,你可以研究 asyncio 模块,了解流行框架如何使用 async/await,并开发自己的异步库。理解委托生成器和 yield from 能让你更深入地了解 Python 处理异步编程的方式。