不看官方文檔,這個問題你可能會一籌莫展

不看官方文檔,這個問題你可能會一籌莫展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攢錢給產品經理買房。

相關文章
相關標籤/搜索