Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 334 – 通过 SuspendIteration 实现简单协程

作者:
Clark C. Evans <cce at clarkevans.com>
状态:
已撤回
类型:
标准轨迹
创建:
2004年8月26日
Python 版本:
3.0
历史记录:


目录

摘要

像 Twisted [1] 和 Peak [2] 这样的异步应用程序框架,基于通过事件队列或延迟执行实现的协作式多任务处理。虽然这种应用程序开发方法不涉及线程,从而避免了一整类问题 [3],但它也带来了不同类型的编程挑战。当 I/O 操作发生阻塞时,用户请求必须挂起,以便其他请求可以继续进行。协程的概念 [4] 有望帮助应用程序开发者应对这种状态管理难题。

本 PEP 提出了一种基于对 迭代器协议 的扩展的有限协程方法。目前,迭代器可以通过引发 StopIteration 异常来指示它已完成值的生成。本提案为此协议添加了另一个异常,即 SuspendIteration,它表示给定的迭代器可能还有更多值要生成,但目前无法生成。

基本原理

目前有两种将协程引入 Python 的方法。Christian Tismer 的 Stackless [6] 涉及通过修改“C”栈来彻底重构 Python 的执行模型。虽然这种方法有效,但其操作难以描述且难以保持可移植性。另一种相关的方法是将 Python 代码编译成 Parrot [7],这是一种基于寄存器的虚拟机,它具有协程。不幸的是,这两种解决方案都不能与 IronPython (CLR) 或 Jython (JavaVM) 兼容。

人们认为,一种更有限的方法,基于迭代器,可以为应用程序程序员提供协程功能,并且仍然可以在各种运行时环境中移植。

  • 迭代器将其状态保存在本地变量中,这些变量不在“C”栈上。迭代器可以被视为类,其状态存储在成员变量中,这些变量在对 next() 方法的每次调用之间都是持久的。
  • 虽然未捕获的异常可能会终止函数的执行,但未捕获的异常不必使迭代器失效。提议的异常 SuspendIteration 利用了此特性。换句话说,仅仅因为一次对 next() 的调用导致了异常,并不一定意味着迭代器本身不再能够生成值。

此新异常会影响四个方面

  • PEP 255 的简单生成器机制可以扩展为安全地“捕获”此 SuspendIteration 异常,存储其当前状态,并将异常传递给调用者。
  • 标准库中的各种迭代器过滤器 [9],例如 itertools.izip,应能够感知此异常,以便能够透明地传播 SuspendIteration。
  • 从 I/O 操作(例如文件或套接字读取器)生成的迭代器可以修改为具有非阻塞变体。如果请求的操作会导致阻塞,则此选项将引发 SuspendIteration 的子类。
  • asyncore 库可以更新为提供一个基本的“运行器”,该运行器从迭代器中提取数据;如果捕获到 SuspendIteration 异常,则它将转到其运行列表中的下一个迭代器 [10]。Twisted 等外部框架将提供替代实现,也许基于 FreeBSD 的 kqueue 或 Linux 的 epoll。

虽然这些看起来像是巨大的变化,但与延续提供的实用程序相比,这是一项非常少量的劳动。

语义

本节将高层次地解释引入此新的 SuspendIteration 异常的行为方式。

简单迭代器

迭代器的当前功能最好通过一个简单的示例来说明,该示例生成两个值“one”和“two”。

class States:

    def __iter__(self):
        self._next = self.state_one
        return self

    def next(self):
        return self._next()

    def state_one(self):
        self._next = self.state_two
        return "one"

    def state_two(self):
        self._next = self.state_stop
        return "two"

    def state_stop(self):
        raise StopIteration

print list(States())

当然,可以使用以下生成器创建等效的迭代:

def States():
    yield 'one'
    yield 'two'

print list(States())

引入 SuspendIteration

假设在生成“one”和“two”之间,上面的生成器可能会阻塞套接字读取。在这种情况下,我们希望引发 SuspendIteration 来表示迭代器尚未完成生成,但目前无法提供值。

from random import randint
from time import sleep

class SuspendIteration(Exception):
      pass

class NonBlockingResource:

    """Randomly unable to produce the second value"""

    def __iter__(self):
        self._next = self.state_one
        return self

    def next(self):
        return self._next()

    def state_one(self):
        self._next = self.state_suspend
        return "one"

    def state_suspend(self):
        rand = randint(1,10)
        if 2 == rand:
            self._next = self.state_two
            return self.state_two()
        raise SuspendIteration()

    def state_two(self):
        self._next = self.state_stop
        return "two"

    def state_stop(self):
        raise StopIteration

def sleeplist(iterator, timeout = .1):
    """
    Do other things (e.g. sleep) while resource is
    unable to provide the next value
    """
    it = iter(iterator)
    retval = []
    while True:
        try:
            retval.append(it.next())
        except SuspendIteration:
            sleep(timeout)
            continue
        except StopIteration:
            break
    return retval

print sleeplist(NonBlockingResource())

在实际情况中,NonBlockingResource 将是一个文件迭代器、套接字句柄或其他基于 I/O 的生成器。sleeplist 将是一个异步反应器,例如 asyncore 或 Twisted 中的反应器。当然,非阻塞资源也可以编写成生成器

def NonBlockingResource():
    yield "one"
    while True:
        rand = randint(1,10)
        if 2 == rand:
            break
        raise SuspendIteration()
    yield "two"

无需添加关键字“suspend”,因为大多数实际内容生成器都不会位于应用程序代码中,它们将位于低级基于 I/O 的操作中。由于大多数程序员不需要接触 SuspendIteration() 机制,因此不需要关键字。

应用迭代器

前面的示例相当牵强,一个更“现实”的示例是一个网页生成器,它生成 HTML 内容并从数据库中提取数据。请注意,这不是“生产者”或“消费者”的示例,而是过滤器的示例。

def ListAlbums(cursor):
    cursor.execute("SELECT title, artist FROM album")
    yield '<html><body><table><tr><td>Title</td><td>Artist</td></tr>'
    for (title, artist) in cursor:
        yield '<tr><td>%s</td><td>%s</td></tr>' % (title, artist)
    yield '</table></body></html>'

当然,问题在于数据库可能在返回任何行之前阻塞一段时间,并且在执行期间,行可能会一次返回 10 或 100 个块。理想情况下,如果数据库阻塞以获取下一组行,则可以服务于另一个用户连接。请注意上述代码中完全没有 SuspendIterator。如果操作正确,应用程序开发人员将能够专注于功能而不是并发问题。

由上述生成器创建的迭代器应该执行必要的魔术来维护状态,但将异常传递给更低级别的异步框架。以下是如何将相应迭代器编码为类的示例

class ListAlbums:

    def __init__(self, cursor):
        self.cursor = cursor

    def __iter__(self):
        self.cursor.execute("SELECT title, artist FROM album")
        self._iter = iter(self._cursor)
        self._next = self.state_head
        return self

    def next(self):
        return self._next()

    def state_head(self):
        self._next = self.state_cursor
        return "<html><body><table><tr><td>\
                Title</td><td>Artist</td></tr>"

    def state_tail(self):
        self._next = self.state_stop
        return "</table></body></html>"

    def state_cursor(self):
        try:
            (title,artist) = self._iter.next()
            return '<tr><td>%s</td><td>%s</td></tr>' % (title, artist)
        except StopIteration:
            self._next = self.state_tail
            return self.next()
        except SuspendIteration:
            # just pass-through
            raise

    def state_stop(self):
        raise StopIteration

复杂因素

虽然上面的示例很简单,但如果中间生成器“压缩”值,即为它生成的每个值提取两个或多个值,则情况会稍微复杂一些。例如,

def pair(iterLeft,iterRight):
    rhs = iter(iterRight)
    lhs = iter(iterLeft)
    while True:
       yield (rhs.next(), lhs.next())

在这种情况下,相应的迭代器行为必须更加微妙,才能处理右侧或左侧迭代器引发 SuspendIteration 的情况。这似乎是分解生成器以识别产生上下文可能发生 SuspendIterator 异常的中间状态的问题。

class pair:

    def __init__(self, iterLeft, iterRight):
        self.iterLeft = iterLeft
        self.iterRight = iterRight

    def __iter__(self):
        self.rhs = iter(iterRight)
        self.lhs = iter(iterLeft)
        self._temp_rhs = None
        self._temp_lhs = None
        self._next = self.state_rhs
        return self

    def next(self):
        return self._next()

    def state_rhs(self):
        self._temp_rhs = self.rhs.next()
        self._next = self.state_lhs
        return self.next()

    def state_lhs(self):
        self._temp_lhs = self.lhs.next()
        self._next = self.state_pair
        return self.next()

    def state_pair(self):
        self._next = self.state_rhs
        return (self._temp_rhs, self._temp_lhs)

本提案假设对于现有的生成器,可以使用这种基于类的编写相应迭代器。挑战似乎在于识别生成器中可能发生挂起的不同状态。

资源清理

当前的生成器机制与异常之间存在奇怪的交互,其中在 try/finally 块中不允许使用“yield”语句。SuspendIterator 异常带来了另一个类似的问题。此问题的影响尚不清楚。但是,可能将生成器重写为状态机,如上一节所述,可以解决此问题,使情况不比以前更糟,甚至可能消除 yield/finally 情况。这方面需要进一步研究。

API 和限制

本提案仅涵盖“挂起”迭代器链,当然不涵盖挂起通用函数、方法或“C”扩展函数。虽然可能无法直接支持在“C”代码中创建生成器,但肯定可以创建符合 SuspendIterator 语义的本机“C”迭代器。

底层实现

PEP 的作者还不熟悉 Python 执行模型,因此无法在此方面发表评论。

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0334.rst

上次修改时间:2023年9月9日 17:39:29 GMT