上下文管理器应该如何用 Python 类型提示进行注释?
import typing
@contextlib.contextmanager
def foo() -> ???:
yield
contextlib 的文档没有太多提及类型。
关于打字的文档。ContextManager也不是很有帮助。
还有 typing.Generator,至少有一个例子。这是否意味着我应该使用
typing.Generator[None, None, None]
而不是 typing.ContextManager
?
import typing
@contextlib.contextmanager
def foo() -> typing.Generator[None, None, None]:
yield
每当我不能 100% 确定函数接受什么类型时,我喜欢查阅 typeshed,它是 Python 类型提示的规范存储库。例如,Mypy 直接捆绑并使用 typeshed 来帮助它执行类型检查。
我们可以在这里找到 contextlib 的存根:https://github.com/python/typeshed/blob/master/stdlib/contextlib.pyi
if sys.version_info >= (3, 2):
class GeneratorContextManager(ContextManager[_T], Generic[_T]):
def __call__(self, func: Callable[..., _T]) -> Callable[..., _T]: ...
def contextmanager(func: Callable[..., Iterator[_T]]) -> Callable[..., GeneratorContextManager[_T]]: ...
else:
def contextmanager(func: Callable[..., Iterator[_T]]) -> Callable[..., ContextManager[_T]]: ...
有点让人不知所措,但我们关心的是这一行:
def contextmanager(func: Callable[..., Iterator[_T]]) -> Callable[..., ContextManager[_T]]: ...
它指出装饰器接受一个
Callable[..., Iterator[_T]]
——一个带有任意参数的函数,返回一些迭代器。所以总而言之,最好这样做:
@contextlib.contextmanager
def foo() -> Iterator[None]:
yield
那么,为什么按照评论的建议使用
Generator[None, None, None]
也有效?
这是因为
Generator
是 Iterator
的子类型——我们可以通过咨询 typeshed 再次自行检查这一点。因此,如果我们的函数返回一个生成器,它仍然与 contextmanager
期望的内容兼容,因此 mypy 可以毫无问题地接受它。
from contextlib import contextmanager
from typing import ContextManager
@contextmanager
def session() -> ContextManager[Session]:
yield Session(...)
UPD:请参阅下面的评论。看起来这个东西让 PyCharm 高兴,但 mypy 不高兴
mypy
检查的方式产生值。根据 contextlib.contextmanager 的Python 3.10 文档
被装饰的函数在调用时必须返回一个生成器迭代器
打字。生成器注释为Generator[YieldType, SendType, ReturnType]
。因此,对于产生
pathlib.Path
的函数,我们可以这样注释我们的函数:
from typing import Generator
from contextlib import contextmanager
@contextmanager
def working_directory() -> Generator[Path, None, None]:
with TemporaryDirectory() as td:
yield Path(td)
但是,不指定 Generators
或
SendType
的
ReturnType
可以注释为
typing.Iterator
:
from typing import Iterator
from contextlib import contextmanager
@contextmanager
def working_directory() -> Iterator[Path]:
with TemporaryDirectory() as td:
yield Path(td)
最后,由于 Python 3.9 采用了 PEP 585 -- 标准集合中的类型提示泛型,typing.Iterator
和
typing.Generator
已被弃用,取而代之的是
collections.abc
实现
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def working_directory() -> Iterator[Path]:
with TemporaryDirectory() as td:
yield Path(td)
@contextmanager
修饰的函数的返回类型是
Iterator[None]
。
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def foo() -> Iterator[None]:
yield
B.上下文管理器本身的类型是AbstractContextManager
:
from contextlib import AbstractContextManager
def make_it_so(context: AbstractContextManager) -> None:
with context:
...
您可能还会看到使用了 typing.ContextManager
,但自 Python 3.9 以来,它已被弃用,取而代之的是
contextlib.AbstractContextManager
。
Iterator[]
版本不起作用。例如下面的代码:
from typing import Iterator
def assert_faster_than(seconds: float) -> Iterator[None]:
return assert_timing(high=seconds)
@contextmanager
def assert_timing(low: float = 0, high: float = None) -> Iterator[None]:
...
会在
return assert_timing(high=seconds)
行产生错误:
Incompatible return value type (got "_GeneratorContextManager[None]", expected "Iterator[None]")
with assert_faster_than(1):
be_quick()
会导致这样的结果:
"Iterator[None]" has no attribute "__enter__"; maybe "__iter__"?
"Iterator[None]" has no attribute "__exit__"; maybe "__next__"?
"Iterator[None]" has no attribute "__enter__"; maybe "__iter__"?
"Iterator[None]" has no attribute "__exit__"; maybe "__next__"?
你可以像这样修复它......
def assert_faster_than(...) -> Iterator[None]:
with assert_timing(...):
yield
但我将使用新的
ContextManager[]
对象来代替,并让装饰器的 mypy 保持沉默:
from typing import ContextManager
def assert_faster_than(seconds: float) -> ContextManager[None]:
return assert_timing(high=seconds)
@contextmanager # type: ignore
def assert_timing(low: float = 0, high: float = None) -> ContextManager[None]:
...
PEP-585,正确的注释类型似乎是AbstractContextManager
(请参阅https://www.python.org/dev/peps/pep-0585/#implementation)。您可以使用以下代码:
import contextlib
@contextlib.contextmanager
def foo() -> contextlib.AbstractContextManager[None]:
yield
这是唯一可以与 PyCharm 正确配合使用的解决方案(以及 typing.ContextManager
,但从 Python 3.9 开始应弃用此解决方案)。当您在
with
语句(类型提示)中使用它时,它会正确地帮助您,这非常有帮助。但是当我回到最初的问题时(
“上下文管理器应该如何用 Python 类型提示进行注释?”)这取决于情况。从我的角度来看,正确的应该是我提到的那个。但这似乎不适用于 mypy(还)。关于此 PEP 有一些更新(请参阅https://github.com/python/mypy/issues/7907),但由于我对 mypy 的经验不多,所以我可能会在这里遗漏一些东西。
class Abstract(ABC):
@abstractmethod
def manager(self) -> ContextManager[None]:
pass
class Concrete(Abstract):
@contextmanager
def manager(self) -> Iterator[None]:
try:
yield
finally:
pass
用 ContextManager[None]
注释抽象方法并用
Iterator[None]
注释实现即可解决问题。