Подсказки типа Python и контекстные менеджеры
Как контекстный менеджер должен быть аннотирован подсказками типов Python?
import typing
@contextlib.contextmanager
def foo() -> ???:
yield
В документации по contextlib documentation on contextlib много не упоминается о типах.
документация по typing.ContextManager не так уж полезна.
Также есть typing.Generator, в котором есть хотя бы пример. Означает ли это, что я должен использовать typing.Generator[None, None, None]
, а не typing.ContextManager
?
import typing
@contextlib.contextmanager
def foo() -> typing.Generator[None, None, None]:
yield
Ответы
Ответ 1
Всякий раз, когда я не уверен на 100%, какие типы принимает функция, я бы хотел обратиться к typeshed, которая является каноническим хранилищем подсказок типов для Python. Mypy, например, связывает и использует типизированные, чтобы помочь ему выполнить проверку типов, например.
Мы можем найти заглушки для contextlib здесь: https://github.com/python/typeshed/blob/master/stdlib/2and3/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
- мы можем снова проверить это сами , посоветовавшись с типами. Таким образом, если наша функция возвращает генератор, она все еще совместима с тем, что ожидает contextmanager
, поэтому mypy принимает его без проблем.
Ответ 2
Версия Iterator[]
не работает, если вы хотите вернуть ссылку на contextmanager. Например, следующий код:
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]:
...
Ответ 3
Возвращаемым типом функции, обернутой менеджером контекста, является Iterator[None]
.
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def foo() -> Iterator[None]:
yield
Ответ 4
С моим PyCharm я делаю следующее, чтобы заставить его подсказку работать:
from contextlib import contextmanager
from typing import ContextManager
@contextmanager
def session() -> ContextManager[Session]:
yield Session(...)