如何在异步循环关闭之前等待对象的__del__完成?

Ron*_*uya 1 python contextmanager python-asyncio aiohttp

我有一个将在其中包含aiohttp.ClientSession对象的类。

通常当您使用

async with aiohttp.ClientSession() as session:  
   # some code
Run Code Online (Sandbox Code Playgroud)

由于调用了会话的__aexit__方法,因此该会话将关闭。

我无法使用上下文管理器,因为我想在对象的整个生命周期中保持会话的持久性。

这有效:

import asyncio
import aiohttp

class MyAPI:
    def __init__(self):
        self.session = aiohttp.ClientSession()

    def __del__(self):
        # Close connection when this object is destroyed
        print('In __del__ now')
        asyncio.shield(self.session.__aexit__(None, None, None))



async def main():
    api = MyAPI()

asyncio.run(main())
Run Code Online (Sandbox Code Playgroud)

但是,如果在某些地方引发了异常,则在完成__aexit__方法之前将关闭事件循环。我该如何克服?

堆栈跟踪:

Traceback (most recent call last):
  File "/home/ron/.PyCharm2018.3/config/scratches/async.py", line 19, in <module>
    asyncio.run(main())
  File "/usr/local/lib/python3.7/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/usr/local/lib/python3.7/asyncio/base_events.py", line 568, in run_until_complete
    return future.result()
  File "/home/ron/.PyCharm2018.3/config/scratches/async.py", line 17, in main
    raise ValueError
ValueError
In __del__ now
Exception ignored in: <function MyAPI.__del__ at 0x7f49982c0e18>
Traceback (most recent call last):
  File "/home/ron/.PyCharm2018.3/config/scratches/async.py", line 11, in __del__
  File "/usr/local/lib/python3.7/asyncio/tasks.py", line 765, in shield
  File "/usr/local/lib/python3.7/asyncio/tasks.py", line 576, in ensure_future
  File "/usr/local/lib/python3.7/asyncio/events.py", line 644, in get_event_loop
RuntimeError: There is no current event loop in thread 'MainThread'.
sys:1: RuntimeWarning: coroutine 'ClientSession.__aexit__' was never awaited
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f49982c2e10>
Run Code Online (Sandbox Code Playgroud)

ala*_*lan 14

正如 @Martijn Pieters 所说,您不能强制事件循环等待对象的__del__析构函数调用。但是,您仍然可以使用__del__析构函数来关闭异步资源,方法是首先检查循环是否正在运行,如果没有则启动新循环。例如,asyncio Redis 模块在析构其 Client 类时使用此技术。具体来说,对于您的代码,析构函数如下:

import asyncio
import aiohttp


class MyAPI:

    def __init__(self):
        self.session = aiohttp.ClientSession()

    def __del__(self):
        # Close connection when this object is destroyed
        try:
            loop = asyncio.get_event_loop()
            if loop.is_running():
                loop.create_task(self.session.close())
            else:
                loop.run_until_complete(self.session.close())
        except Exception:
            pass
Run Code Online (Sandbox Code Playgroud)


Mar*_*ers 5

不要使用__del__钩子来清理异步资源。您根本不能指望它被调用,更不用说控制何时使用它,或者那时异步循环是否仍然可用。您确实想明确地处理此问题。

要么使该API成为异步上下文管理器,要么在退出时使用finally处理程序明确清除资源,例如;在withasync with报表设计基本上以传统的封装处理资源清理finally块。

我将在API此处将实例设置为上下文管理器:

class MyAPI:
    def __init__(self):
        self.session = aiohttp.ClientSession()

    async def __aenter__(self):
        return self

    async def __aexit__(self, *excinfo):
        await self.session.close()
Run Code Online (Sandbox Code Playgroud)

请注意,ClientSession.__aexit__()实际上所有操作都在等待self.close(),因此以上内容直接适用于该协程。

然后在您的主循环中使用:

async def main():
    async with MyAPI() as api:
        pass
Run Code Online (Sandbox Code Playgroud)

另一种选择是向MyAPI实例提供您自己的会话对象,并在完成时自行负责关闭它:

class MyAPI:
    def __init__(self, session):
        self.session = session

async def main():
    session = aiohttp.ClientSession()
    try:
        api = MyAPI(session)
        # do things with the API
    finally:
        await session.close()
Run Code Online (Sandbox Code Playgroud)