英文:
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。
我有上面的GoodWrapperType和BadWrapperType。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() -> GoodWrapperType:
@contextmanager
def inner(some_int: int, some_str: str) -> 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
如果我们不使用生成器的 send 和 return 特性,可以简化其类型为 Iterator[<yield type>],如上所示。
AbstractContextManager[<enter return type>] 是上下文管理器的泛型类型。(注意:在 Python 3.9 之前,您应该使用 from typing import ContextManager)
由于我们的内部函数不产生任何内容,因此在这两种情况下的类型参数都是 None。
然而,contextmanager 返回一个"上下文装饰器",这是一个具有双重用途的类,既可以作为上下文管理器,也可以作为装饰器使用。使用 AbstractContextManager 进行类型化将会隐藏结果的装饰器特性,使其无法通过下游类型检查来识别。
如果您需要这个特性,那么您最初的方法使用 _GeneratorContextManager 可能是最好的方式。(有一个公共的 contextlib.ContextDecorator 基类,但是 mypy 不支持交集类型,因此只使用公开导出的类型来同时将某物类型化为 AbstractContextManager 和 ContextDecorator 并不方便)。
英文:
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() -> WrapperType:
@contextmanager
def inner(some_int: int, some_str: str) -> Iterator[None]:
# Do things with some_int, some_str
yield
return inner
If we're not using the send and return features of the generator we can simplify its type to just Iterator[<yield type>] as above.
AbstractContextManager[<enter return type>] 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).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论