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

Posted

技术标签:

【中文标题】Python 类型提示和上下文管理器【英文标题】:Python type hints and context managers 【发布时间】:2018-09-18 21:31:13 【问题描述】:

上下文管理器应该如何使用 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

【问题讨论】:

它是一个生成器,它产生、发送和返回None,所以它是Generator[None, None, None]。是否将它用于上下文管理器并不重要。 如果你对这个特定的上下文管理器的用途有任何想法,你可以为预期的类型进行注释,否则你几乎可以接受任何东西(甚至没有) 在我的具体情况下,我只想使用上下文管理器进行日志记录(计时),因此产量、发送和返回值确实是 None 【参考方案1】:

当我不能 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

那么,为什么按照 cmets 的建议使用 Generator[None, None, None] 也有效?

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

【讨论】:

查看potential dupe,我遇到了这个答案。似乎上下文管理器中使用的生成器的返回类型应该反映上下文管理器返回的内容,即ContextManager[_T]。这样,我的 IDE 中的静态检查器就能够成功地推断出上下文变量的类型,但它不适用于 Iterator。你可以检查吗?我想将另一个问题标记为骗子,但就目前而言,这个答案并不能解决另一个问题中的问题。【参考方案2】:

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

from contextlib import contextmanager
from typing import ContextManager

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

UPD:见下面的 cmets。看起来这件事让 PyC​​harm 开心,但 mypy 不开心

【讨论】:

这似乎对我不起作用。 Mypy 说 error: The return type of a generator function should be "Generator" or one of its supertypeserror: Argument 1 to "contextmanager" has incompatible type "Callable[[Abc, Any, Any], ContextManager[Any]]"; expected "Callable[..., Iterator[<nothing>]]" 我猜 mypy 太严格了 :D 我目前没有更好的注释 多亏了这一点,类型提示现在对我有用。 PyCharm(2020.1.2社区版)和python 3.8。 谢谢,这对 PyCharm 有帮助,但对 mypy 没有帮助。也许尚不存在让这两种工具都满意的单一解决方案 @kolypto 不,不是 mypy 太严格。 PyCharm 是完全错误的。您应该将其注释为生成器,装饰器将获取该生成器并返回一个 ContextManager。【参考方案3】:

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

【讨论】:

您希望assert_faster_thanassert_timing 的类型签名看起来相同,但您只将@contextmanager 应用于其中一个。我认为正确的做法是声明assert_faster_than(...) -> ContextManager[None],但声明assert_timing(..) -> Iterator[None]【参考方案4】:

基于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:此 PEP 适用于 Python 3.9 和更新版本,如果您想在较旧的 Python 版本(从 3.7 开始)上尝试此功能,您必须使用 from __future__ import annotations 才能向前兼容。跨度> 【参考方案5】:

上下文管理器包装的函数的返回类型是Iterator[None]

from contextlib import contextmanager
from typing import Iterator

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

【讨论】:

【参考方案6】:

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

被修饰的函数在调用时必须返回一个generator-iterator

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)

【讨论】:

【参考方案7】:

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

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 类型提示和上下文管理器的主要内容,如果未能解决你的问题,请参考以下文章

python---基础知识回顾进程和线程(自定义线程池,上下文管理器和协程的使用)

Python 多态 对象常用内置函数 运算符重载 对象迭代器 上下文管理

Python 中 with 上下文管理器

python unittest 将断言与上下文管理器结合起来

Python高级详解with语句和上下文管理器

Python高级详解with语句和上下文管理器