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

问题描述 投票:0回答:7

上下文管理器应该如何用 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
python python-typing mypy
7个回答
75
投票

每当我不能 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 可以毫无问题地接受它。


53
投票
使用 PyCharm,我执行以下操作以使其类型提示发挥作用:

from contextlib import contextmanager from typing import ContextManager @contextmanager def session() -> ContextManager[Session]: yield Session(...)
UPD:请参阅下面的评论。看起来这个东西让 PyCharm 高兴,但 mypy 不高兴


35
投票
我在这里没有找到关于注释 contextmanagers 的好答案,这些上下文管理器以通过 Python 3.10 下的

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)
    

17
投票
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)

行产生错误:

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]: ...
    

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 的经验不多,所以我可能会在这里遗漏一些东西。


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]
 注释实现即可解决问题。

© www.soinside.com 2019 - 2024. All rights reserved.