正确地为返回生成器表达式的可调用对象进行类型标注。

huangapple go评论76阅读模式
英文:

Properly type a callable returning a Generator expression

问题

我有以下的代码片段

```python
from contextlib import _GeneratorContextManager, contextmanager

GoodWrapperType = Callable[[int, str], _GeneratorContextManager[None]]
BadWrapperType = Callable[[int, str], Generator[None, None, None]]

def wrapper() -> GoodWrapperType:

    @contextmanager
    def inner(some_int: int, some_str: str) -> Generator[None, None, None]:
        # 对 some_int 和 some_str 进行操作
        yield

    return inner

我想在pytest测试套件的上下文中执行此操作,在这个上下文中,inner会注入一些fixture。

我有上面的GoodWrapperTypeBadWrapperType。Pylance告诉我不能将BadWrapperType分配为wrapper的返回类型。我已经找到了一种使用_GeneratorContextManager的解决方法,但由于它以下划线开头,我不应该导入它。

是否有更好的方法?正确的方法是什么?

我在想是否没有直接的解决方案(至少我没有找到),这是否表明我不应该在Python中这样做。


<details>
<summary>英文:</summary>

I have the following snippet of code

```python
from contextlib import _GeneratorContextManager, contextmanager

GoodWrapperType = Callable[[int, str], _GeneratorContextManager[None]]
BadWrapperType = Callable[[int, str], Generator[None, None, None]]

def wrapper() -&gt; GoodWrapperType:

    @contextmanager
    def inner(some_int: int, some_str: str) -&gt; Generator[None, None, None]:
        # Do things with some_int, some_str
        yield

    return inner

I want to do this in the context of a pytest testing suite, where the wrapper is getting some fixture injected into the inner.

I have above the GoodWrapperType, and the BadWrapperType. Pylance is telling me that I can't assign the BadWrapperType as the return type of my wrapper. I have found a solution with the GoodWrapperType, using _GeneratorContextManager, but since it's prefixed with an _, I am not supposed to be importing it.

Is there a better way ? What's the proper way of doing this ?

I was wondering if the fact that there's no straight-forward solution (that I've found anyway), might be a hint that I shouldn't do this in Python.

答案1

得分: 2

wrapper 不返回一个返回生成器函数的可调用对象,它返回一个返回上下文管理器的可调用对象,这是一个具有 __enter____exit__ 方法的类。

@contextmanager 装饰器会从应用到生成器函数的地方构建这个类,但最终结果不是一个生成器。

因此,我们希望将 inner 类型化为返回生成器函数,但将 wrapper 类型化为返回一个可调用对象,该可调用对象返回应用装饰器后的上下文管理器。

以下是经过类型检查的版本:

from contextlib import contextmanager, AbstractContextManager
from typing import Callable, Iterator, ContextManager

WrapperType = Callable[[int, str], AbstractContextManager[None]]

def wrapper() -> WrapperType:

    @contextmanager
    def inner(some_int: int, some_str: str) -> Iterator[None]:
        # 对 some_int 和 some_str 进行操作
        yield

    return inner

如果我们不使用生成器的 sendreturn 特性,可以简化其类型为 Iterator[<yield type>],如上所示。

AbstractContextManager[<enter return type>] 是上下文管理器的泛型类型。(注意:在 Python 3.9 之前,您应该使用 from typing import ContextManager

由于我们的内部函数不产生任何内容,因此在这两种情况下的类型参数都是 None

然而,contextmanager 返回一个"上下文装饰器",这是一个具有双重用途的类,既可以作为上下文管理器,也可以作为装饰器使用。使用 AbstractContextManager 进行类型化将会隐藏结果的装饰器特性,使其无法通过下游类型检查来识别。

如果您需要这个特性,那么您最初的方法使用 _GeneratorContextManager 可能是最好的方式。(有一个公共的 contextlib.ContextDecorator 基类,但是 mypy 不支持交集类型,因此只使用公开导出的类型来同时将某物类型化为 AbstractContextManagerContextDecorator 并不方便)。

英文:

wrapper doesn't return callable returning a generator function, it returns a callable returning a context manager, which is a class with __enter__ and __exit__ methods.

@contextmanager decorator builds this class from the generator function it is applied to, but the end result is not a generator.

So we want to type inner as returning a generator function, but wrapper as returning a callable which returns the context manager that is the result of applying the decorator.

Here is a version which type-checks:

from contextlib import contextmanager, AbstractContextManager
from typing import Callable, Iterator, ContextManager


WrapperType = Callable[[int, str], AbstractContextManager[None]]


def wrapper() -&gt; WrapperType:

    @contextmanager
    def inner(some_int: int, some_str: str) -&gt; Iterator[None]:
        # Do things with some_int, some_str
        yield

    return inner

https://mypy-play.net/?mypy=latest&amp;python=3.11&amp;flags=strict&amp;gist=6b2b3360a4916ec654a84ac84fcea544

If we're not using the send and return features of the generator we can simplify its type to just Iterator[&lt;yield type&gt;] as above.

AbstractContextManager[&lt;enter return type&gt;] is a generic type for context managers. (Note: pre Python 3.9 you should use from typing import ContextManager instead)

Since our inner function yields nothing, the type param in both cases is None.

However contextmanager returns a "context decorator", a dual-purpose class that can function as either a context manager or a decorator. Typing with AbstractContextManager will hide the decorator-ness of the result from downstream type checks.

If you need that property then your original approach with _GeneratorContextManager is probably the best way. (There is a public contextlib.ContextDecorator base class but mypy lacks intersection types so there's no convenient way to type something as both an AbstractContextManager & ContextDecorator using only the publicly exported types).

huangapple
  • 本文由 发表于 2023年8月9日 18:28:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/76866869.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定