Python coroutines: Release context manager when pausing

后端 未结 1 1710
执笔经年
执笔经年 2020-12-31 14:44

Background: I\'m a very experienced Python programmer who is completely clueless about the new coroutines/async/await features. I can\'t write an async \"hello world\" to sa

相关标签:
1条回答
  • 2020-12-31 15:37

    Coroutines are built on iterators - the __await__ special method is a regular iterator. This allows you to wrap the underlying iterator in yet another iterator. The trick is that you must unwrap the iterator of your target using its __await__, then re-wrap your own iterator using your own __await__.

    The core functionality that works on instantiated coroutines looks like this:

    class CoroWrapper:
        """Wrap ``target`` to have every send issued in a ``context``"""
        def __init__(self, target: 'Coroutine', context: 'ContextManager'):
            self.target = target
            self.context = context
    
        # wrap an iterator for use with 'await'
        def __await__(self):
            # unwrap the underlying iterator
            target_iter = self.target.__await__()
            # emulate 'yield from'
            iter_send, iter_throw = target_iter.send, target_iter.throw
            send, message = iter_send, None
            while True:
                # communicate with the target coroutine
                try:
                    with self.context:
                        signal = send(message)
                except StopIteration as err:
                    return err.value
                else:
                    send = iter_send
                # communicate with the ambient event loop
                try:
                    message = yield signal
                except BaseException as err:
                    send, message = iter_throw, err
    

    Note that this explicitly works on a Coroutine, not an Awaitable - Coroutine.__await__ implements the generator interface. In theory, an Awaitable does not necessarily provide __await__().send or __await__().throw.

    This is enough to pass messages in and out:

    import asyncio
    
    
    class PrintContext:
        def __enter__(self):
            print('enter')
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            print('exit via', exc_type)
            return False
    
    
    async def main_coro():
        print(
            'wrapper returned',
            await CoroWrapper(test_coro(), PrintContext())
        )
    
    
    async def test_coro(delay=0.5):
        await asyncio.sleep(delay)
        return 2
    
    asyncio.run(main_coro())
    # enter
    # exit via None
    # enter
    # exit <class 'StopIteration'>
    # wrapper returned 2
    

    You can delegate the wrapping part to a separate decorator. This also ensures that you have an actual coroutine, not a custom class - some async libraries require this.

    from functools import wraps
    
    
    def send_context(context: 'ContextManager'):
        """Wrap a coroutine to issue every send in a context"""
        def coro_wrapper(target: 'Callable[..., Coroutine]') -> 'Callable[..., Coroutine]':
            @wraps(target)
            async def context_coroutine(*args, **kwargs):
                return await CoroWrapper(target(*args, **kwargs), context)
            return context_coroutine
        return coro_wrapper
    

    This allows you to directly decorate a coroutine function:

    @send_context(PrintContext())
    async def test_coro(delay=0.5):
        await asyncio.sleep(delay)
        return 2
    
    print('async run returned:', asyncio.run(test_coro()))
    # enter
    # exit via None
    # enter
    # exit via <class 'StopIteration'>
    # async run returned: 2
    
    0 讨论(0)
提交回复
热议问题