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

108

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

import typing

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

contextlib文档中没有多少提到类型的内容。

typing.ContextManager 文档也没有提供太多帮助。

还有 typing.Generator ,至少有一个示例。这是不是意味着我应该使用 typing.Generator[None, None, None]而不是typing.ContextManager呢?

import typing

@contextlib.contextmanager
def foo() -> typing.Generator[None, None, None]:
    yield

2
它是一个生成器,可以产生、发送和返回None,因此它是一个Generator[None, None, None]。无论您是否将其用于上下文管理器都没有关系。 - internet_user
如果您对此特定上下文管理器的使用有任何想法,可以为期望的类型进行注释,否则您将接受任何东西(甚至是None)。 - Onilol
在我的特定情况下,我只想使用上下文管理器来记录日志(时间),因此yield、send和return值确实是None - Peter
contextlib中的文档没有提到任何类型,因为装饰器没有类型提示:https://github.com/python/cpython/blob/f192a558f538489ad1be30aa145e71d942798d1c/Lib/contextlib.py#L260。许多静态代码分析器假定装饰器不会改变类型提示。 - Philip Couling
7个回答

70

每当我不确定一个函数接受哪些类型时,我会查阅 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 的子类型——我们可以通过查看 typeshed 中的定义来进行确认。因此,如果我们的函数返回一个生成器,它仍然与 contextmanager 所期望的兼容,所以 mypy 会毫无问题地接受它。


9
我看了一个可能是重复的问题,并找到了这个答案。它似乎表明在使用上下文管理器中的生成器时,返回类型应该反映上下文管理器的返回类型,即 ContextManager[_T]。有了这个,我的 IDE 静态检查器能够成功地推断出上下文变量的类型,而使用 Iterator 无法做到这一点。你能检查一下吗?我想将另一个问题标记为重复,但目前为止,这个答案并不能解决其他问题中的问题。 - shmee
@shmee,我不确定我同意“在上下文管理器中使用的生成器的返回类型应该反映上下文管理器的返回值”的说法。函数返回什么就返回什么,我通常认为装饰器是修改函数的,所以如果你想知道装饰后的函数返回什么,你需要查看装饰器的类型注释。 - Dustin Wyatt
Iterator 的功能是正确的,但在语义上感觉不太对。 - jamesdlin
@jamesdlin 是的,因为它并不是所有情况下都是迭代器。它可以是任何东西...它可以是一个对象,一个字符串,任何东西都可以被yielded。 - Ryan Glenn
这个答案的缺点是许多工具不使用typeshed,而是假定@contextmanager通过类型提示未经修改地传递。因此,如果你提示以取悦Mypy,你会发现像mkdocstrings-python这样的工具在你的文档中生成了不正确的签名。不幸的是,在那些类型提示不包含在Python源代码中的地方,使用来自typeshed的类型提示作为规范已经在使工具达成一致方面造成了一些混乱。 - Philip Couling
谢谢@PhilipCouling的各种评论。我觉得它们非常有帮助。 - Myridium

50

我使用PyCharm,按照以下步骤使其类型提示生效:

from contextlib import contextmanager
from typing import ContextManager

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

更新:请查看下方评论。看起来这个东西可以让PyCharm感到满意,但不适用于mypy。


19
这对我似乎不起作用。Mypy显示“错误:生成器函数的返回类型应为“Generator”或其超类型之一”,以及“错误:'contextmanager'的第1个参数具有不兼容的类型'Callable [[Abc,Any,Any],ContextManager [Any]]'; 期望的是“Callable […,Iterator [<nothing>]]”。 - CMCDragonkai
1
我猜mypy太严格了:D目前我没有更好的注释。 - kolypto
2
现在,由于这个,类型提示对我来说已经可以工作了。使用 PyCharm(2020.1.2 社区版)和 Python 3.8。 - Robino
12
不是mypy太严格了,而是PyCharm错了。你应该将其注释为生成器(Generator),装饰器会将其作为生成器并返回一个上下文管理器(ContextManager)。 - Neil G
1
@PhilipCouling 我今天想了一下,我明白你说类型检查器可以自由地做任何它们想做的事情。例如,现在很少有类型检查器实际上使用描述符协议(__get__)的注释。然而,我认为 typeshed 给出的 contextmanager 的注释是正确的。我认为不使用它并假设所有装饰器只是让它们的函数通过是“不可取的”,正如你所承认的那样。你纠正我的措辞是对的——“错误”太过苛刻。 - Neil G
显示剩余6条评论

29

我在这里没有找到一个好的答案,关于如何注释yield值的上下文管理器,使其在Python 3.10下通过mypy检查。根据Python 3.10 contextlib.contextmanager文档所述:

被装饰的函数在调用时必须返回一个生成器迭代器

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)

自从Python 3.9采用了PEP 585 - 标准集合中的类型提示泛型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)

2
谢谢,这个答案太棒了。我可以确认Generator[T, None, None]是正确的类型注解。MyPy对此非常满意。(哦,关于将其注释为ContextManager[T]的其他答案是100%错误的,所以不要听他们的。听James的! :) 附言:我没有尝试你提到的collections.abc.Iterator[T],因为它依赖于Python 3.9+,但那看起来更好。 - Mitch McMabers
@MitchMcMabers我们知道ContextManager是错误的,但它让PyCharm感到高兴,因为自2019年以来他们还没有修复这该死的bug - undefined

14

A. 被 @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代替。


7
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)这行会产生错误:

返回值类型不兼容(得到了"_GeneratorContextManager[None]",期望的是"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]:
    ...

9
你希望assert_faster_thanassert_timing的类型注释看起来相同,但是你只给其中一个应用了@contextmanager。我认为正确的做法是声明assert_faster_than(...) -> ContextManager[None],而 assert_timing(..) -> Iterator[None] - Marius Gedminas

6

根据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不太熟悉,可能会漏掉一些东西。


这段代码在运行时会导致 Python 3.7.9 报错:TypeError: 'ABCMeta' object is not subscriptable - levsa
1
@levsa:这个PEP适用于Python 3.9及更高版本,如果您想在旧版本的Python(从3.7开始)中尝试此功能,则必须使用“from future import annotations”以实现向前兼容。 - Nerxis
1
PEP-585提到typing.ContextManager已被弃用,推荐使用contextlib.AbstractContextManager。但它并没有说明这是一个适合使用@contextlib.contextmanager装饰的函数的正确注释类型。 - Yacine Nouri
这个 PyCharm 问题似乎与以下相关:https://youtrack.jetbrains.com/issue/PY-36444/PyCharm-doesnt-infer-types-when-using-contextlib.contextmanager-decorator - Yacine Nouri

0

当我实现抽象方法时,遇到了类似的问题:

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]实现解决了这个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接