如何在IPython启动文件中运行异步代码?
首先得说清楚为什么你直接在启动文件里写await会报错——这和IPython的启动流程直接相关:
IPython的启动脚本是在交互式环境完全初始化之前运行的,这时候它还没开启「顶层await支持」。你在交互式shell里能直接写await foo(),是因为IPython会偷偷把你的输入代码包装成一个异步函数,然后用它自己管理的事件循环来执行;但启动文件的代码是直接在普通的同步Python上下文中跑的,就像你在普通.py脚本里直接写await一样,肯定会报「不在异步函数里」的语法错误。
而且你不想用asyncio.run()是完全正确的,因为它会创建一个独立的新事件循环,跑完就直接关掉,这样你初始化的Tortoise-ORM连接之类的上下文就带不到后续的交互式shell里了,完全达不到启动初始化的目的。
那怎么才能复用IPython后续要用来交互的那个事件循环,在启动阶段跑异步代码呢?给你两个靠谱的方案:
方案1:用IPython的run_cell直接执行带await的代码
IPython的run_cell方法会用和交互式shell完全一样的上下文处理代码,自动帮你搞定顶层await的问题,而且会复用同一个事件循环。直接在启动文件里这么写:
# .ipython/profile_default/startup/01_hello.py print("hello!") async def foo(): print("foo") # 这里可以加入你的异步初始化逻辑,比如Tortoise-ORM的初始化代码 # 获取当前IPython实例 from IPython import get_ipython ip = get_ipython() # 确保我们在IPython环境中(避免脚本单独运行时报错) if ip is not None: # 用run_cell执行带await的代码,这会复用IPython的事件循环 ip.run_cell("await foo()") # 如果是初始化Tortoise,就写 ip.run_cell("await init_tortoise()")
运行之后你会看到预期的效果:
$ ipython Python 3.12.0 (main, Nov 12 2023, 10:40:37) [GCC 11.4.0] Type 'copyright', 'credits' or 'license' for more information IPython 8.31.0 -- An enhanced Interactive Python. Type '?' for help. hello foo In [1]:
这样异步代码跑完后,事件循环会保留下来,你后续在交互式shell里用Tortoise-ORM之类的工具时,初始化的上下文也能正常复用。
方案2:用IPython的启动钩子,等事件循环就绪再执行
如果你的异步初始化逻辑比较复杂,或者担心启动阶段IPython的事件循环还没完全准备好,可以用IPython的post_run_startup钩子——这个钩子会在所有启动脚本跑完、交互式环境彻底准备好之后触发,这时候事件循环已经完全就绪:
# .ipython/profile_default/startup/01_init.py import asyncio from IPython import get_ipython async def async_init(): print("Starting async initialization...") # 这里写你的异步初始化逻辑,比如Tortoise-ORM的连接 # await Tortoise.init(...) await asyncio.sleep(0.2) # 模拟异步操作 print("Async initialization done!") # 获取IPython实例 ip = get_ipython() if ip is not None: # 定义一个回调函数,当启动完成后执行 def trigger_async_init(_): # 获取IPython正在使用的事件循环 loop = asyncio.get_running_loop() # 把异步任务提交到这个循环里 loop.create_task(async_init()) # 注册钩子:post_run_startup 会在所有启动脚本执行完后触发 ip.events.register("post_run_startup", trigger_async_init)
这个方案更稳妥,因为它确保你的异步代码是在IPython的事件循环完全启动后才执行的,上下文和后续的交互式shell完全一致。
总结一下:核心就是别在启动文件里直接写await,而是借助IPython自己的工具(run_cell或者启动钩子),让异步代码跑在它的事件循环上下文里,这样既不会报错,又能保留初始化的上下文。
备注:内容来源于stack exchange,提问作者odigity

