Python协程:暂停时释放上下文管理器

时间:2019-05-10 13:14:38

标签: python async-await coroutine

背景:我是一位非常有经验的Python程序员,他对新的协程/异步/等待功能一无所知。我无法编写一个异步的“ hello world”来挽救生命。

我的问题是:给我一个任意的协程函数f。我想编写一个协程函数g,该函数将包装f,即将g给用户,就好像它是f,然后用户将其调用并最好不要使用g,因为f将在后台使用def generator_wrapper(_, *args, **kwargs): gen = function(*args, **kwargs) method, incoming = gen.send, None while True: with self: outgoing = method(incoming) try: method, incoming = gen.send, (yield outgoing) except Exception as e: method, incoming = gen.throw, e 。就像装饰普通的Python函数以添加功能时一样。

我要添加的功能:每当程序流进入协程时,它都会获取我提供的上下文管理器,并且一旦程序流离开协程,它将释放该上下文管理器。流量又回来了吗?重新获取上下文管理器。它退出了吗?重新释放它。直到协程完全完成。

为了演示,这是使用普通生成器描述的功能:

// Change email_admin based on subject using a hidden field
add_filter( 'ninja_forms_submit_data', function ( $form_data ) {
    $form_model = Ninja_Forms()->form( $form_data[ 'id' ] );
    foreach( $form_data[ 'fields' ] as $field ) { // Loop through fields
        $field_model = $form_model->get_field( $field[ 'id' ] );
        $field_key = $field_model->get_setting( 'key' );
        $field_id = $field[ 'id' ];
        if( 'subject_value' == $field_key ) {
            $subject_value = '';
            $subject_value = $form_data[ 'fields' ][ $field_id ][ 'value' ]; // Take subject dropdown value.
            $email_redirect = get_string_between($subject_value, '{', '}'); // Get email from subject value.
            $new_subject = delete_all_between($subject_value, '{', '}'); // Remove {email} from subject value.
            $form_data[ 'fields' ][ $field_id ][ 'value' ] = $new_subject; // Assign new subject value.
        } elseif ( 'subject_send' == $field_key ) {
            if ( isset($email_redirect) )
                $form_data[ 'fields' ][ $field_id ][ 'value' ] = $email_redirect; // Give hidden field the email which is used in send to field.
        } else {
            continue;
        }
    }
    return $form_data;
} );

可以用协程吗?

1 个答案:

答案 0 :(得分:4)

协程基于迭代器构建-__await__ special method是常规迭代器。这使您可以将基础迭代器包装在另一个迭代器中。诀窍是您必须使用目标__await__ 解包目标迭代器,然后使用自己的__await__ 重新包装您自己的迭代器。

可用于实例化协程的核心功能如下:

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.args[0] if err.args else None
            else:
                send = iter_send
            # communicate with the ambient event loop
            try:
                message = yield signal
            except BaseException as err:
                send, message = iter_throw, err

请注意,这明确适用于Coroutine,而不是Awaitable-Coroutine.__await__实现了生成器接口。从理论上讲,Awaitable不一定提供__await__().send__await__().throw

这足以传递消息进出:

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

您可以将包装部分委派给单独的装饰器。这还可以确保您拥有实际的协程,而不是自定义类-一些异步库需要这样做。

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

这允许您直接装饰协程函数:

@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