Python 类型提示和上下文管理器

Python type hints and context managers

上下文管理器应如何使用 Python 类型提示进行注释?

import typing

@contextlib.contextmanager
def foo() -> ???:
    yield

documentation on contextlib 没有太多提及类型。

documentation on typing.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] 也像评论所建议的那样有效?

这是因为 GeneratorIterator 的子类型——我们可以自己再次检查一下 by consulting typeshed。所以,如果我们的函数 returns 是一个生成器,它仍然与 contextmanager 期望的兼容,所以 mypy 可以毫无问题地接受它。

当您想 return 上下文管理器的引用时,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]:
    ...

一个。 @contextmanager修饰的函数的return类型是Iterator[None].

from contextlib import contextmanager
from typing import Iterator

@contextmanager
def foo() -> Iterator[None]:
    yield

乙。上下文管理器本身的类型是 AbstractContextManager:

from contextlib import AbstractContextManager

def make_it_so(context: AbstractContextManager) -> None:
    with context:
        ...

您可能还会看到使用了 typing.ContextManager,但自 Python 3.9 以来 deprecated 支持 contextlib.AbstractContextManager

对于我的 PyCharm,我执行以下操作以使其类型提示起作用:

from contextlib import contextmanager
from typing import ContextManager

@contextmanager
def session() -> ContextManager[Session]:
    yield Session(...)

UPD:请参阅下面的评论。看起来这个东西让 PyCharm 快乐,但不是 mypy

基于PEP-585 the correct annotation type seems to be AbstractContextManager (see 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] 注释实现解决了问题。

我在这里找不到关于注释上下文管理器的好答案,这些上下文管理器以通过 Python 3.10 下的 mypy 检查的方式产生值。根据 Python 3.10 documentation for contextlib.contextmanager

The function being decorated must return a generator-iterator when called

typing.Generators被注释为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)

但是,未指定 SendTypeReturnTypeGenerators 可以改为注释为 typing.Iterator:

from typing import Iterator
from contextlib import contextmanager

@contextmanager
def working_directory() -> Iterator[Path]:
    with TemporaryDirectory() as td:
        yield Path(td)

最后,由于 PEP 585 -- Type Hinting Generics In Standard Collections 在 Python 3.9 中被采用,typing.Iteratortyping.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)