0

函数对派生自bigop(init, report)的大型动态内部数据结构进行操作,并接受可调用的. 函数返回当前状态的摘要。datainitreport(data)status(data)data

函数bigop调用每个主要步骤report的当前状态,data依次调用status. data为每个步骤复制(或使其持久化)会很昂贵,因此report必须在每个步骤完成才能bigop继续。

函数view(gen)接受生成gen状态摘要的连续值的生成器,并在生成时显示每个值的可视化。该函数view根据迄今为止生成的值维护内部状态。(在我的特殊情况下,可以复制这种内部状态,但最好避免。)

假设函数bigop不能view改变。

问题:如何定义genreport和 一个程序main,以便bigop在 上运行,并到达每个主要步骤init时显示状态报告值的可视化? bigop

难点就在于此,report并且gen在其他函数内部被调用,所以通常的 Python 协程模式是不适用的。(在我的特殊情况下,bigop实际上是一个生成器。)

之前关于使用回调从普通函数生成生成器的问题是使用线程回答的,但我想知道是否有更简单的方法。

注意:只有与 Python 2.7 兼容的答案对我有用;但如果差异是相关的,我很想看看 Python 3 的答案。

def bigop(init, report):
    data = init
    while data < 10:           # complicated condition
        print 'working ...'
        data += 1              # complicated operation
        report(data)

def view(gen):
    for value in gen:
        print value            # complicated display routine

def main(init):
    """
    example:

    >> main(7)
    'working ...'
    8
    'working ...'
    9
    'working ...'
    10
    """
    pass

问题:如何定义main

4

1 回答 1

1

给定您的示例代码:

def main(init):
    def report(x):
        print x
    bigop(init, report)

但是,我认为这不是您要在这里寻找的。大概你想reportview某种方式输入数据。

你可以通过扭转局面来做到这一点——它不是view一个驱动另一个生成器的生成器,而是一个由调用send它的外部调用者驱动的生成器。例如:

def view():
    while True:
        value = yield
        print value
def main(init):
    v = view()
    v.next()
    def report(x):
        v.send(x)
    bigop(init, report)

但是你说view不能改变。当然,您可以随时编写一个viewdriveryield对象send。或者,更简单地说,只是重复调用view([data])并让它遍历单个对象。

无论如何,我看不出你期望这有什么帮助。bigop不是协程,你不能把它变成一个协程。鉴于此,没有办法强制它与其他协程合作共享。

如果要同时交错处理和报告,则必须使用线程(或进程)。而且“报告必须在 BIGOP 继续之前的每个步骤完成”这一事实已经是您要求的一部分,这意味着您无论如何都不能安全地在这里并发任何事情,所以我不确定您在寻找什么。

如果你只是想在没有并发的情况下交错处理和报告——或者定期挂钩bigop,或其他类似的事情——你可以使用协程来做到这一点,但它与使用子程序的效果完全相同——上面的两个例子差不多相等的。所以,你只是无缘无故地增加了复杂性。

(如果bigop受 I/O 限制,您可以使用 greenlets,并猴子修补 I/O 操作以将它们异步化,就像gevent这样eventlet做。但如果它受 CPU 限制,那么这样做没有任何好处。)


详细说明这个viewdriver想法:我上面描述的内容相当于view([data])每次都打电话,所以它对你没有帮助。如果你想让它成为一个迭代器,你可以,但它只会导致阻塞bigop或旋转view,因为你试图用消费者来喂养消费者。

作为生成器可能很难理解,所以让我们将它构建为一个类:

class Reporter(object):
    def __init__(self):
        self.data_queue = []
        self.viewer = view(self)
    def __call__(self, data):
        self.data_queue.append(data)
    def __iter__(self):
        return self
    def __next__(self):
        return self.data_queue.pop()

bigop(init, Reporter())

每次bigop调用report(data)时,都会调用我们的__call__,将一个新元素添加到我们的队列中。每次view通过循环,它都会调用 our __next__,从队列中弹出一个元素。如果bigop保证比 快view,一切都会工作,但第一次view领先,它会得到一个IndexError.

解决这个问题的唯一方法是__next__尝试直到data_queue非空。但只是这样做将永远旋转,而不是让bigop工作来产生一个新元素。而且你不能做成__next__一个生成器,因为view期望一个迭代器对值,而不是一个迭代器迭代器。

幸运的是,__call__它可以是一个生成器,因为bigop它不关心它返回什么值。所以,你可以扭转局面。但你不能那样做,因为那样就没有什么东西可以驱动那台发电机了。

因此,您必须在迭代下方添加另一层协程。然后,__next__可以等待 a next_coro(通过调用next它),它产生 acall_coro然后产生它得到的值。同时,__call__也必须send相同call_coro,等待它,然后让步。

到目前为止,这并没有改变任何事情,因为你有两个例程都在尝试驱动next_coro,其中一个 ( __next__) 没有阻塞其他任何地方,所以它只会旋转——它的next调用看起来像send(None)from __call__

解决此问题的唯一方法是构建蹦床(PEP 342包括通用蹦床的源代码,尽管在这种情况下您可以构建更简单的专用蹦床),安排next_corocall_coro明确交替,确保next_coro正确处理之间的交替两个不同的入口点,然后run__next__(和__init__)驱动调度程序。

使困惑?在本周的剧集之后,你不会是……不,我在开谁的玩笑。你会感到困惑。写下所有这些是一回事。调试它是另一个。(特别是因为每个重要的堆栈跟踪都立即在蹦床上终止。)所有这些工作对你有什么好处?与使用 greenlets 或线程完全相同的好处,具有完全相同的缺点。

由于您最初的问题是是否有比使用线程更简单的方法,所以答案是:不,没有。

于 2013-06-04T22:55:41.727 回答