【问题标题】:How to correctly specify type hints with AsyncGenerator and AsyncContextManager如何使用 AsyncGenerator 和 AsyncContextManager 正确指定类型提示
【发布时间】:2021-10-24 14:03:53
【问题描述】:

考虑下面的代码

import contextlib
import abc
import asyncio

from typing import AsyncContextManager, AsyncGenerator, AsyncIterator


class Base:

    @abc.abstractmethod
    async def subscribe(self) -> AsyncContextManager[AsyncGenerator[int, None]]:
        pass

class Impl1(Base):

    @contextlib.asynccontextmanager
    async def subscribe(self) ->  AsyncIterator[ AsyncGenerator[int, None] ]: <-- mypy error here

        async def _generator():
            for i in range(5):
                await asyncio.sleep(1)
                yield i
                    
        yield _generator()

对于Impl1.subscribe mypy 给出错误

Signature of "subscribe" incompatible with supertype "Base"

在上述情况下指定类型提示的正确方法是什么?还是 mypy 错了?

【问题讨论】:

  • 作为一种解决方法,您可以将def _generator() 抽象化并在Base' by calling _generator(). Then in Impl1` 中实现subscribe 实现def _generator()
  • 您甚至不需要将_generator 放入Base,您可以在没有装饰器的情况下在Impl1 中拥有一个私人_subscribe(其中包含您当前@987654333 的内容) @) 然后从实际的subscribe 返回contextlib.asynccontextmanager(self._subcribe())。我怀疑问题是 mypy 不喜欢你用装饰器更改类型签名,这看起来很奇怪,因为它是一个 stdlib 装饰器
  • @Wombatz 我简化了代码。在我的代码中,yield generator() 在 try 中受到保护,除了子类特定的初始化和清理代码。所以这是行不通的。
  • @joel 没错,我记得读到 mypy 原则上应该与异步上下文管理器一起使用。但这可能是一个错误,或者我犯了一个错误。

标签: python type-hinting mypy


【解决方案1】:

刚好遇到同样的问题,当天就发现了这个问题,但也很快想出了答案。

您需要从抽象方法中删除async

为了解释原因,我将把案例简化为一个简单的异步迭代器:

@abc.abstractmethod
async def foo(self) -> AsyncIterator[int]:
    pass

async def v1(self) -> AsyncIterator[int]:
    yield 0

async def v2(self) -> AsyncIterator[int]:
    return v1()

如果您比较 v1 和 v2,您会发现函数签名看起来相同,但它们实际上做了非常不同的事情。 v2 兼容抽象方法,v1 不兼容。

当您添加async 关键字时,mypy 将函数的返回类型推断为Coroutine。但是,如果你还放了一个yield,它就会推断出返回类型是AsyncIterator

reveal_type(foo)
# -> typing.Coroutine[Any, Any, typing.AsyncIterator[builtins.int]]
reveal_type(v1)
# -> typing.AsyncIterator[builtins.int]
reveal_type(v2)
# -> typing.Coroutine[Any, Any, typing.AsyncIterator[builtins.int]]

如您所见,抽象方法中缺少yield 意味着这被推断为Coroutine[..., AsyncIterator[int]]。换句话说,像async for i in await v2(): 这样使用的函数。

通过删除async:

@abc.abstractmethod
def foo(self) -> AsyncIterator[int]:
    pass
reveal_type(foo)
# -> typing.AsyncIterator[builtins.int]

我们看到返回类型现在是 AsyncIterator 并且现在与 v1 兼容,而不是 v2。换句话说,像async for i in v1():这样使用的函数

你也可以看到这和v1基本一样:

def v3(self) -> AsyncIterator[int]:
    return v1()

虽然语法不同,但 v3 和 v1 都是函数,调用时将返回 AsyncIterator,考虑到我们实际上返回的是 v1() 的结果,这一点应该很明显。

【讨论】:

  • 感谢您分享这一见解。在考虑了您的答案之后,现在一切都说得通了。所以基本问题是,对于抽象例程,mypy 无法在生成器和普通函数之间进行推断(因为缺少 yield)。或者换句话说,defasync def 对象具有不同的类型,具体取决于 yield 是否在其主体中。我知道从抽象例程中省略 async 可以解决问题,但我觉得它更像是一种解决方法,而不是“正确”的解决方案(由于提到的问题,它可能不存在)。
  • async def with yield 实际上不是协程而是异步生成器,反之亦然。
  • 是的,但带有yielddef 也是生成器函数,而不是普通函数。问题是生成器功能的遗留设计。 Python 从yield 的存在推断它们,而不是显式的。这是使用 asyncio 避免的设计,其中需要显式的async,而不是从await 推断。如果它是显式的,那么您的抽象方法可能看起来像 async gen def 或类似的东西,问题就不会发生。
  • 那么下面的评估是否正确?没有办法通过类型提示将异步生成器与返回异步迭代器的异步函数区分开来?
  • 返回异步迭代器的函数(非异步)与异步生成器相同(本质上是语法糖)。返回异步迭代器的异步函数是另一回事,请重新阅读上面的示例(v1()v2())以及必须如何更改抽象方法以匹配其中一个。
猜你喜欢
  • 2023-03-29
  • 2019-08-12
  • 2016-02-29
  • 2020-11-17
  • 2017-01-18
  • 2022-09-30
  • 1970-01-01
  • 1970-01-01
  • 2016-10-16
相关资源
最近更新 更多