Python`with` context与generator / coroutines / tasks

时间:2017-04-18 00:23:59

标签: python coroutine

我想尝试使用python with块来将修饰符应用于该块中的操作。但我不确定在协同程序存在的情况下是否可以做到这一点。

例如,假设我有一个WithContext对象暂时按下这样的堆栈:

class WithContext:
    stack = []
    def __init__(self, val):
        self.val = val
    def __enter__(self):
        WithContext.stack.append(self.val)
    def __exit__(self, exc_type, exc_val, exc_tb):
        WithContext.stack.pop()
def do_scoped_contextual_thing():
    print(WithContext.stack[-1])

(显然堆栈成员必须是线程本地的,但暂时忽略它。)

然后这段代码:

with WithContext("a"):
    do_scoped_contextual_thing()
    with WithContext("b"):
        do_scoped_contextual_thing()
with WithContext("c"):
   do_scoped_contextual_thing()

将打印:

a
b
c

但现在假设我有一个协程情况:

def coroutine():
    with WithContext("inside"):
        yield 1
        do_scoped_contextual_thing()
        yield 2

with WithContext("outside"):
    for e in coroutine():
        do_scoped_contextual_thing()
        print("got " + str(e))

希望此代码输出:

outside
got 1
inside
outside
got 2

但实际上它会输出:

inside
got 1
inside
inside
got 2

外部变为内部,因为协程内的__enter__将值放在堆栈顶部,并且在协程结束之前不调用__exit__(而不是不断地进入和退出 - 当你跳进和离开协程时。)

有没有办法解决这个问题?是否存在“coroutine-local”变量?

3 个答案:

答案 0 :(得分:1)

我对此感觉不太好,但我确实修改了你的测试代码,重新输入了几次协同程序。与@ CraigGidney的解决方案类似,它使用inspect模块访问和缓存有关创建WithContext对象的调用堆栈(也就是“范围”)的信息。

然后我基本上搜索堆栈以查找缓存值,并使用id函数尝试避免保持对实际帧对象的引用。

import inspect

class WithContext:
    stack = []
    frame_to_stack = {}
    def __init__(self, val):
        self.val = val
    def __enter__(self):
        stk = inspect.stack(context=3)
        caller_id = id(stk[1].frame)
        WithContext.frame_to_stack[caller_id] = len(WithContext.stack)
        WithContext.stack.append( (caller_id, self.val))

    def __exit__(self, exc_type, exc_val, exc_tb):
        wc = WithContext.stack.pop()
        del WithContext.frame_to_stack[wc[0]]

def do_scoped_contextual_thing():
    stack = inspect.stack(context=0)
    f2s = WithContext.frame_to_stack

    for f in stack:
        wcx = f2s.get(id(f.frame))

        if wcx is not None:
            break
    else:
        raise ValueError("No context object in scope.")

    print(WithContext.stack[wcx][1])

def coroutine():
    with WithContext("inside"):
        for n in range(3):
            yield 1
            do_scoped_contextual_thing()
            yield 2

with WithContext("outside"):
    for e in coroutine():
        do_scoped_contextual_thing()
        print("got " + str(e))

答案 1 :(得分:0)

一个可能的半破解“解决方案”是将上下文与堆栈框架的位置相关联,并在查找上下文时检查该位置。

class WithContext:
    _stacks = defaultdict(list)

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

    def __enter__(self):
        _, file, _, method, _, _ = inspect.stack()[1]
        WithContext._stacks[(file, method)].append(self.val)

    def __exit__(self, exc_type, exc_val, exc_tb):
        _, file, _, method, _, _ = inspect.stack()[1]
        WithContext._stacks[(file, method)].pop()

    @staticmethod
    def get_context():
        for frame in inspect.stack()[1:]:
            _, file, _, method, _, _ = frame
            r = WithContext._stacks[(file, method)]
            if r:
                return r[-1]
        raise ValueError("no context")

请注意,不断查找堆栈帧比仅传递值更昂贵,并且您可能不想告诉别人您写这个。

请注意,在更复杂的情况下,这仍然会中断。

例如:

  • 如果同一个方法在堆栈上两次怎么办?
  • 如果生成器从一个地方迭代一下,然后从另一个地方再多一点怎么办?
  • 递归生成器怎么样?
  • 异步方法怎么样?

答案 2 :(得分:0)

我遇到了同样的问题。基本上我希望能够在进入/离开协程的运行上下文时执行代码,在我的情况下,即使在交错yield s的情况下,也要保持一个正确的调用堆栈。事实证明,tornadoStackContext的形式支持此问题,可以按如下方式使用:

@gen.coroutine
def correct():
    yield run_with_stack_context(StackContext(ctx), other_coroutine)

其中ctx是在事件循环执行enter时将exitother_coroutine的上下文管理器。

请参阅https://github.com/muhrin/plumpy/blob/8d6cd97d8b521e42f124e77b08bb34c8375cd1b8/plumpy/processes.py#L467了解我如何使用它。

我没有研究过实现,但龙卷风v5切换到使用asyncio作为默认事件循环,因此它也应与之兼容。