【问题标题】:Asynchronous context manager异步上下文管理器
【发布时间】:2016-09-22 19:45:09
【问题描述】:

我有一个asynchronous API,我用它来连接并发送邮件到一个 SMTP 服务器,该服务器有一些设置和拆卸。因此,它非常适合使用 Python 3 的 contextlib 中的 contextmanager

虽然,我不知道是否可以写,因为他们都使用生成器语法来写。

这可能说明了问题(包含 yield-base 和 async-await 语法的混合,以说明异步调用和向上下文管理器屈服之间的区别)。

@contextmanager
async def smtp_connection():
    client = SMTPAsync()
    ...

    try:
        await client.connect(smtp_url, smtp_port)
        await client.starttls()
        await client.login(smtp_username, smtp_password)
        yield client
    finally:
        await client.quit()

目前在python中这种事情可能吗?如果是,我将如何使用with as 语句?如果没有,是否有其他方法可以实现这一点 - 也许使用旧式上下文管理器?

【问题讨论】:

  • asyncio 还引入了async with 异步上下文管理器协议,请参阅:python.org/dev/peps/pep-0492/…
  • 这看起来正是我想要的。当我有机会时会尝试实施它。
  • 从 3.7 开始(2018 年发布),contextlib 将有 @asynccontextmanager

标签: python asynchronous contextmanager


【解决方案1】:

感谢@jonrsharpe 能够制作异步上下文管理器。

对于任何想要一些示例代码的人来说,这就是我最终的样子:

class SMTPConnection():
    def __init__(self, url, port, username, password):
        self.client   = SMTPAsync()
        self.url      = url
        self.port     = port
        self.username = username
        self.password = password

    async def __aenter__(self):
        await self.client.connect(self.url, self.port)
        await self.client.starttls()
        await self.client.login(self.username, self.password)

        return self.client

    async def __aexit__(self, exc_type, exc, tb):
        await self.client.quit()

用法:

async with SMTPConnection(url, port, username, password) as client:
    await client.sendmail(...)

如果我做了什么愚蠢的事情,请随时指出。

【讨论】:

  • 问题是如果你同时使用两次,你的第二个进入的客户端会覆盖第一个,第一个的退出也会退出第二个。
  • @iScrE4m 啊,是的,我没想到它会被多次使用,只是按需创建一个实例,供一次性使用。也许可以为此创建一个包装类,将 __aenter____aexit__ 委托给新实例。
  • 记得在__aexit__ 中处理异常。否则你会隐藏异常并得到奇怪的隐形错误。
  • AIU 这构成“手动执行”,asynccontextmanager 是实现这一目标的现代方式,所以这个答案可能应该被标记为过时?
【解决方案2】:

asyncio_extras 包有一个很好的解决方案:

import asyncio_extras

@asyncio_extras.async_contextmanager
async def smtp_connection():
    client = SMTPAsync()
    ...

对于 Python async_generator 包并将 yield client 替换为 await yield_(client)

【讨论】:

    【解决方案3】:

    从 Python 3.7 开始,您可以编写:

    from contextlib import asynccontextmanager
    
    @asynccontextmanager
    async def smtp_connection():
        client = SMTPAsync()
        ...
    
        try:
            await client.connect(smtp_url, smtp_port)
            await client.starttls()
            await client.login(smtp_username, smtp_password)
            yield client
        finally:
            await client.quit()
    

    在 3.7 之前,您可以为此使用 async_generator 包。在 3.6 上,你可以写:

    # This import changed, everything else is the same
    from async_generator import asynccontextmanager
    
    @asynccontextmanager
    async def smtp_connection():
        client = SMTPAsync()
        ...
    
        try:
            await client.connect(smtp_url, smtp_port)
            await client.starttls()
            await client.login(smtp_username, smtp_password)
            yield client
        finally:
            await client.quit()
    

    如果你想一直工作回到 3.5,你可以这样写:

    # This import changed again:
    from async_generator import asynccontextmanager, async_generator, yield_
    
    @asynccontextmanager
    @async_generator      # <-- added this
    async def smtp_connection():
        client = SMTPAsync()
        ...
    
        try:
            await client.connect(smtp_url, smtp_port)
            await client.starttls()
            await client.login(smtp_username, smtp_password)
            await yield_(client)    # <-- this line changed
        finally:
            await client.quit()
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-10
      • 1970-01-01
      • 2022-11-10
      • 2022-07-28
      • 2021-05-26
      • 2020-06-07
      相关资源
      最近更新 更多