不看官方文档,这个问题你可能会一筹莫展

不看官方文档,这个问题你可能会一筹莫展html

收录于话题
#你不知道的 Python
71个

不看官方文档,这个问题你可能会一筹莫展
摄影:产品经理
产品经理亲自下厨作的鸡 jio jio
在 Python 3.7版本开始,引入了新功能asyncio.run来快速运行一段异步代码。python

例如对于一段使用 aiohttp 请求网址的代码,在 Python 3.6或者以前的版本,咱们是这样写的:git

import asyncio
import aiohttp

async def main():
    async with aiohttp.ClientSession() as client:
        resp = await client.get('http://httpbin.org/ip')
        ip = await resp.json()
        print(ip)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

运行效果以下图所示:
不看官方文档,这个问题你可能会一筹莫展github

如今有了asyncio.run,咱们能够少敲几回键盘:mongodb

import asyncio
import aiohttp

async def main():
    async with aiohttp.ClientSession() as client:
        resp = await client.get('http://httpbin.org/ip')
        ip = await resp.json()
        print(ip)

asyncio.run(main())

运行效果以下图所示:
不看官方文档,这个问题你可能会一筹莫展json

这个功能真是太方便了!我准备彻底不使用老式写法了。直到有一天,我使用 Motor 读取数据。api

Motor 是用来异步读写 MongoDB 的库。我写代码通常会先写一段 Demo,确认没有问题了再把 Demo 改为正式代码。咱们用 Motor写一段读取 MongoDB 的代码:app

import asyncio
import motor.motor_asyncio

async def main():
    client = motor.motor_asyncio.AsyncIOMotorClient()
    db = client.exercise
    collection = db.person_info
    async for doc in collection.find({}, {'_id': 0}):
        print(doc)

asyncio.run(main())

运行效果符合预期,以下图所示:
不看官方文档,这个问题你可能会一筹莫展框架

既然 Demo 能够正常运行,那么咱们把这段代码修改得稍微正式一些,使用类来包住正常的代码:异步

import asyncio
import motor.motor_asyncio

class MongoUtil:
    def __init__(self):
        conn = motor.motor_asyncio.AsyncIOMotorClient()
        db = conn.exercise
        self.collection = db.person_info

    async def read_people(self):
        async for doc in self.collection.find({}, {'_id': 0}):
            print(doc)

util = MongoUtil()
asyncio.run(util.read_people())

运行效果以下图所示,居然报错了:
不看官方文档,这个问题你可能会一筹莫展

报错信息的最后一句,单独摘录出来::

RuntimeError: Task <Task pending coro=<MongoUtil.read_people() running at /Users/kingname/test_fastapi/test_motor.py:12> cb=[_run_until_complete_cb() at /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/asyncio/base_events.py:158]> got Future <Future pending cb=[run_on_executor.._call_check_cancel() at /Users/kingname/.virtualenvs/test_fastapi-v5CW09hz/lib/python3.7/site-packages/motor/frameworks/asyncio/init.py:80]> attached to a different loop”

其中最关键的一句话是:attached to a different loop

显然咱们这个程序是单进程单线程的程序,这段报错说明当前的这个线程里面,在运行asyncio.run以前,就已经存在一个事件循环了。而根据 asyncio 的规定,一个线程里面只能有一个事件循环正在运行,因此就致使报错。

为了解释这个报错的缘由,咱们来看看 Python 的官方文档中,asyncio.run的相关说明[1],以下图所示:
不看官方文档,这个问题你可能会一筹莫展

其中画红色方框的两个地方:

This function cannot be called when another asyncio event loop is running in the same thread. 当另外一个 asyncio 事件循环正在当前线程运行的时候,不能调用这个函数。”
This function always creates a new event loop and closes it at the end. 这个函数老是建立一个新的事件循环并在最后(运行完成)关闭它。”

因此,当咱们调用asyncio.run的时候,必须确保当前线程没有事件循环正在运行。

可是,当咱们在运行上图第16行代码,初始化MongoUtil的时候,它的构造函数init会运行,因而第7行代码就会运行。

来看一下Motor 的官方文档中关于AsyncIOMotorClient的描述[2]
不看官方文档,这个问题你可能会一筹莫展

AsyncIOMotorClient有一个参数叫作io_loop,若是不传入事件循环对象的话,就会使用默认的。但程序运行到这个位置的时候,尚未谁建立了事件循环,因而Motor就会本身建立一个事件循环。

关于这一点,你们能够阅读Motor 的源代码[3]第150-154行:
不看官方文档,这个问题你可能会一筹莫展

在不传入io_loop的时候,会调用self._framework.get_event_loop()。其中,self._framework多是trio也多是asyncio。由于 Motor 支持这两种异步框架。咱们这里使用的是asyncio。因为当前没有正在运行的事件循环,因此asyncio.get_event_loop就会建立一个,并让它运行起来。

因此当咱们使用 Motor 初始化 MongoDB 的链接时,就已经建立了一个事件循环了。但当代码运行到asyncio.run的时候,又准备建立一个新的事件循环,天然而然程序就运行错了。

因此,要让程序正常运行,咱们在最后一行不能建立新的事件循环,而是须要获取由 Motor 已经建立好的事件循环。因此代码须要改为老式写法:

loop = asyncio.get_event_loop()
loop.run_until_complete(util.read_people())

这样一来,程序就能正常工做了:
不看官方文档,这个问题你可能会一筹莫展

这个问题经过官方文档就能找到缘由并解决。但若是你不看官方文档,而是一味在网上乱搜索,恐怕很难找到解决办法。

参考资料

[1]
相关说明: https://docs.python.org/3/library/asyncio-task.html#asyncio.run
[2]
描述: https://motor.readthedocs.io/en/stable/api-asyncio/asyncio_motor_client.html#motor.motor_asyncio.AsyncIOMotorClient
[3]
源代码: https://github.com/mongodb/motor/blob/4c7534c6200e4f160268ea6c0e8a9038dcc69e0f/motor/core.py#L154

不看官方文档,这个问题你可能会一筹莫展

kingname攒钱给产品经理买房。

相关文章
相关标签/搜索