python 中的异步执行

async exec in python

我想在异步函数中调用 exec 并执行类似于以下代码(无效)的操作:

import asyncio

async def f():
    await exec('x = 1\n' 'await asyncio.sleep(x)')

更准确地说,我希望能够在 exec 中运行的代码中等待未来。

如何实现?

你的问题是你正试图等待 None 对象 - exec 忽略其代码中的 return 值,并且总是 returns None. 如果你想执行并等待结果,你应该使用 eval- eval returns 给定表达式的值。

您的代码应如下所示:

import asyncio

async def f():
    exec('x = 1')
    await eval('asyncio.sleep(x)')

loop = asyncio.get_event_loop()
loop.run_until_complete(f())
loop.close()

感谢所有建议。我发现这可以通过异步的 greenlets 来完成,因为 greenlets 允许执行 "top level await":

import greenlet
import asyncio

class GreenAwait:
    def __init__(self, child):
        self.current = greenlet.getcurrent()
        self.value = None
        self.child = child

    def __call__(self, future):
        self.value = future
        self.current.switch()

    def __iter__(self):
        while self.value is not None:
            yield self.value
            self.value = None
            self.child.switch()

def gexec(code):
    child = greenlet.greenlet(exec)
    gawait = GreenAwait(child)
    child.switch(code, {'gawait': gawait})
    yield from gawait

async def aexec(code):
    green = greenlet.greenlet(gexec)
    gen = green.switch(code)
    for future in gen:
        await future

# modified asyncio example from Python docs
CODE = ('import asyncio\n'
        'import datetime\n'

        'async def display_date():\n'
        '    for i in range(5):\n'
        '        print(datetime.datetime.now())\n'
        '        await asyncio.sleep(1)\n')

def loop():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(aexec(CODE + 'gawait(display_date())'))
    loop.close()

Note: F-strings are only supported in python 3.6+. For older versions, use %s, .format() or the classic + concatenation.

async def aexec(code):
    # Make an async function with the code and `exec` it
    exec(
        f'async def __ex(): ' +
        ''.join(f'\n {l}' for l in code.split('\n'))
    )

    # Get `__ex` from local variables, call it and return the result
    return await locals()['__ex']()

已知问题:

  • 如果您在字符串中使用换行符(三引号),会弄乱格式。

这是基于@YouTwitFace 的回答,但保持全局变量不变,更好地处理局部变量并传递 kwargs。注意 multi-line 字符串仍然不会保留其格式。也许你想要 this?

async def aexec(code, **kwargs):
    # Don't clutter locals
    locs = {}
    # Restore globals later
    globs = globals().copy()
    args = ", ".join(list(kwargs.keys()))
    exec(f"async def func({args}):\n    " + code.replace("\n", "\n    "), {}, locs)
    # Don't expect it to return from the coro.
    result = await locs["func"](**kwargs)
    try:
        globals().clear()
        # Inconsistent state
    finally:
        globals().update(**globs)
    return result

首先要拯救当地人。它声明函数,但具有受限的本地名称空间,因此它不会触及在 aexec 帮助程序中声明的内容。该函数名为 func 并且我们访问 locs dict,其中包含 exec 的本地结果。 locs["func"] 是我们想要执行的,因此我们在 aexec 调用中用 **kwargs 调用它,这会将这些 args 移动到本地名称空间中。然后我们等待这个并将其存储为 result。最后,我们恢复 locals 和 return 结果。

Warning:

Do not use this if there is any multi-threaded code touching global variables. Go for @YouTwitFace's answer which is simpler and thread-safe, or remove the globals save/restore code

这是一个 module 使用 A​​ST 来做事。这意味着多行字符串将完美地工作并且行号将与原始语句匹配。另外,如果任何东西是一个表达式,它被返回(如果有多个,则作为一个列表,否则只是一个元素)

我制作了这个模块(查看此答案的修订历史以了解有关内部工作的更多详细信息)。我用它here

这是使用内置 ast 模块的更可靠的方法:

import ast

async def async_exec(stmts, env=None):
    parsed_stmts = ast.parse(stmts)

    fn_name = "_async_exec_f"

    fn = f"async def {fn_name}(): pass"
    parsed_fn = ast.parse(fn)

    for node in parsed_stmts.body:
        ast.increment_lineno(node)

    parsed_fn.body[0].body = parsed_stmts.body
    exec(compile(parsed_fn, filename="<ast>", mode="exec"), env)

    return await eval(f"{fn_name}()", env)

只需使用这个功能:

import asyncio
async def async_exec(code):
    t = [None]
    exec('async def _async_exec():\n return {}\nt[0] = asyncio.ensure_future(_async_exec())'.format(code))
    return await t[0]

这里有一个代码示例,可以直接运行。 (适用于 Python 3.6.8)

import asyncio
async def async_exec(code):
    t = [None]
    exec('async def _async_exec():\n return {}\nt[0] = asyncio.ensure_future(_async_exec())'.format(code))
    return await t[0]

async def p(s):
    await asyncio.sleep(s)
    return s


async def main():
    print(await async_exec('await p(0.1) / await p(0.2)'))


asyncio.get_event_loop().run_until_complete(main())

我试着解释一下,在exec中定义一个async函数。在异步函数中,运行 你想要的代码。但是 exec 没有 return 值,使用 t[0] 存储一个 asyncio 未来,在 exec 之外等待未来以获得 return 值。