重要提示

此文档涵盖 IPython 6.0 及更高版本。从 6.0 版本开始,IPython 不再支持与低于 3.3 的 Python 版本(包括所有版本的 Python 2.7)的兼容性。

如果您正在寻找与 Python 2.7 兼容的 IPython 版本,请使用 IPython 5.x LTS 版本并参考其文档(LTS 是长期支持版本)。

REPL 中的异步:自动等待

注意

此功能为实验性功能,其行为可在 python 和 IPython 版本之间发生更改,无需事先弃用。

从 IPython 7.0 开始,当使用 Python 3.6 及更高版本时,IPython 提供从 REPL 运行异步代码的功能。在 Python REPL 中为 SyntaxError 的构造可在 IPython 中无缝使用。

此处给出的示例适用于终端 IPython,在笔记本界面或使用 Jupyter 协议的任何其他前端中运行异步代码需要 IPykernel 5.0 或更高版本。异步代码在 IPykernel 中的运行方式的详细信息将在 IPython、IPykernel 及其版本之间有所不同。

当使用受支持的库时,IPython 将自动允许 REPL 中的 Future 和协程被 await。如果在顶级作用域中使用了 await(或任何其他异步构造,如 async-with、async-for),或者如果仅在 async def 函数上下文中有效的任何结构存在,则会发生这种情况。例如,以下内容在 Python REPL 中为语法错误

Python 3.6.0
[GCC 4.2.1]
Type "help", "copyright", "credits" or "license" for more information.
>>> import aiohttp
>>> session = aiohttp.ClientSession()
>>> result = session.get('https://api.github.com')
>>> response = await result
  File "<stdin>", line 1
    response = await result
                          ^
SyntaxError: invalid syntax

应在 IPython REPL 中按预期执行

Python 3.6.0
Type 'copyright', 'credits' or 'license' for more information
IPython 7.0.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import aiohttp
   ...: session = aiohttp.ClientSession()
   ...: result = session.get('https://api.github.com')

In [2]: response = await result
<pause for a few 100s ms>

In [3]: await response.json()
Out[3]:
{'authorizations_url': 'https://api.github.com/authorizations',
 'code_search_url': 'https://api.github.com/search/code?q={query}...',
...
}

您可以使用 c.InteractiveShell.autoawait 配置选项并将其设置为 False 以停用异步代码的自动包装。您还可以使用 %autoawait 魔术在运行时切换行为

In [1]: %autoawait False

In [2]: %autoawait
IPython autoawait is `Off`, and set to use `asyncio`

默认情况下,IPython 将假定与 Python 提供的 asyncio 集成,但提供了与其他库的集成。特别是,我们提供了与 curiotrio 库的实验性集成。

您可以使用 c.InteractiveShell.loop_runner 选项或 autoawait <name integration> 魔术切换当前集成。

例如

In [1]: %autoawait trio

In [2]: import trio

In [3]: async def child(i):
   ...:     print("   child %s goes to sleep"%i)
   ...:     await trio.sleep(2)
   ...:     print("   child %s wakes up"%i)

In [4]: print('parent start')
   ...: async with trio.open_nursery() as n:
   ...:     for i in range(5):
   ...:         n.spawn(child, i)
   ...: print('parent end')
parent start
   child 2 goes to sleep
   child 0 goes to sleep
   child 3 goes to sleep
   child 1 goes to sleep
   child 4 goes to sleep
   <about 2 seconds pause>
   child 2 wakes up
   child 1 wakes up
   child 0 wakes up
   child 3 wakes up
   child 4 wakes up
parent end

在上述示例中,顶级作用域中的 async with 在 Python 中是语法错误。

如果与 IPython 的其他功能和各种已注册扩展一起使用,使用此模式可能会产生意外后果。特别是,如果您是 AST 转换器的直接或间接用户,则这些转换器可能不适用于您的代码。

在使用命令行 IPython 时,默认循环(或运行器)不会在后台处理,因此顶级异步代码必须完成,REPL 才会允许您输入更多代码。与通常的 Python 语义一样,仅在第一次等待时才启动可等待项。也就是说,在第一个示例中,在 In[1]In[2] 之间没有执行任何网络请求。

对 IPython.embed() 的影响

由于 IPython 核心是异步的,因此使用 IPython.embed() 现在需要一个循环来运行。默认情况下,IPython 将使用一个假的协程运行器,该运行器应该允许嵌套 IPython.embed()。不过,在使用 IPython 嵌入时,这将阻止使用 %autoawait 功能。

如果您想运行异步代码,可以为 embed() 显式设置一个协程运行器,尽管确切的行为是未定义的。

对 Magics 的影响

几个 Magics(%%timeit%timeit%%time%%prun)尚未更新为与异步代码一起使用,并且在尝试使用顶级 await 时会引发语法错误。我们欢迎任何有助于解决这些问题和我们尚未发现的额外案例的贡献。我们希望在核心 Python 中更好地支持顶级异步代码。

内部

由于在交互式 REPL 中不支持运行异步代码(截至 Python 3.7),我们不得不依赖于许多复杂的工作方式和启发式方法来实现这一点。了解其工作原理很有意思,以便理解潜在的错误,或提供自定义运行器。

在我们可用的众多方法中,我们只找到一种适合我们的需求。在底层,我们使用来自 async-def 函数的代码对象,并在修改它以不创建新的 locals() 作用域后在全局名称空间中运行它。

async def inner_async():
    locals().update(**global_namespace)
    #
    # here is user code
    #
    return last_user_statement
codeobj = modify(inner_async.__code__)
coroutine = eval(codeobj, user_ns)
display(loop_runner(coroutine))

您会注意到的第一件事是,与经典的 exec 不同,只有一个名称空间。其次,用户代码在函数作用域中运行,而不是在模块作用域中运行。

除了上述内容之外,对 function 的 AST 进行了重大修改,并且 loop_runner 可以任意复杂。因此,这种代码的开销很大。

默认情况下,如果 async 模式被认为是必需的,则生成的协程函数将被 Asyncio 的 loop_runner = asyncio.get_event_loop().run_until_complete() 方法使用,否则协程将只是在一个简单的运行器中耗尽。不过,可以更改默认运行器。

循环运行器是一个同步函数,负责运行协程对象。

运行器负责确保 coroutine 运行到完成,并且它应该返回执行协程的结果。让我们为 trio 编写一个运行器,在用作练习时打印一条消息,trio 是特殊的,因为它通常更喜欢运行一个函数对象并自己生成一个协程,我们可以通过将其包装在一个没有参数的 async-def 中并将其值传递给 trio 来绕过此限制

In [1]: import trio
   ...: from types import CoroutineType
   ...:
   ...: def trio_runner(coro:CoroutineType):
   ...:     print('running asynchronous code')
   ...:     async def corowrap(coro):
   ...:         return await coro
   ...:     return trio.run(corowrap, coro)

我们可以通过将其传递给 %autoawait 来设置它

In [2]: %autoawait trio_runner

In [3]: async def async_hello(name):
   ...:     await trio.sleep(1)
   ...:     print(f'Hello {name} world !')
   ...:     await trio.sleep(1)

In [4]: await async_hello('async')
running asynchronous code
Hello async world !

python 中的异步编程(尤其是在 REPL 中)仍然是一个相对较新的主题。我们预计一些代码不会按你的预期运行,因此请随时为这个代码库做出改进并向我们提供反馈。

我们邀请你彻底测试此功能并报告任何意外行为以及提出任何改进建议。

在笔记本(IPykernel)中使用 Autoawait

将 ipykernel 更新到 5.0 或更高版本

pip install ipykernel ipython --upgrade
# or
conda install ipykernel ipython --upgrade

这应该会自动启用 %autoawait 集成。与终端 IPython 不同,所有代码都在 asyncio 事件循环上运行,因此手动创建循环将不起作用,包括使用诸如 %run 的魔术或其他自己创建事件循环的框架。在这些情况下,你可以尝试使用诸如 nest_asyncio 的项目,并关注 此讨论

终端 IPython 和 IPykernel 之间的差异

确切的异步代码运行行为在终端 IPython 和 IPykernel 之间有所不同。此行为的根本原因是 IPykernel 具有正在运行的持久 asyncio 循环,而终端 IPython 为每个代码块启动并停止一个循环。如果你习惯自己操作 asyncio 循环,这在某些情况下可能会导致令人惊讶的行为,例如,请参阅 #11303 以进行更深入的讨论,但这里有一些令人惊讶的情况。

此行为是实现细节,不应依赖它。它可以在 IPython 的未来版本中不发出警告的情况下更改。

在终端 IPython 中,仅当存在顶级异步代码时,才会为每个代码块启动一个循环

$ ipython
In [1]: import asyncio
   ...: asyncio.get_event_loop()
Out[1]: <_UnixSelectorEventLoop running=False closed=False debug=False>

In [2]:

In [2]: import asyncio
   ...: await asyncio.sleep(0)
   ...: asyncio.get_event_loop()
Out[2]: <_UnixSelectorEventLoop running=True closed=False debug=False>

请注意,running 仅在我们 await sleep() 的情况下为 True

在使用 ipykernel 的笔记本中,asyncio 事件循环始终在运行

$ jupyter notebook
In [1]: import asyncio
   ...: loop1 = asyncio.get_event_loop()
   ...: loop1
Out[1]: <_UnixSelectorEventLoop running=True closed=False debug=False>

In [2]: loop2 = asyncio.get_event_loop()
   ...: loop2
Out[2]: <_UnixSelectorEventLoop running=True closed=False debug=False>

In [3]: loop1 is loop2
Out[3]: True

在终端 IPython 中,仅当前台任务正在运行且仅当前台任务是异步任务时,才会处理后台任务

$ ipython
In [1]: import asyncio
   ...:
   ...: async def repeat(msg, n):
   ...:     for i in range(n):
   ...:         print(f"{msg} {i}")
   ...:         await asyncio.sleep(1)
   ...:     return f"{msg} done"
   ...:
   ...: asyncio.ensure_future(repeat("background", 10))
Out[1]: <Task pending coro=<repeat() running at <ipython-input-1-02d0ef250fe7>:3>>

In [2]: await asyncio.sleep(3)
background 0
background 1
background 2
background 3

In [3]: import time
...: time.sleep(5)

In [4]: await asyncio.sleep(3)
background 4
background 5
background 6g

在使用 IPykernel 的笔记本、QtConsole 或任何其他前端中,后台任务应按预期运行。