Comment un gestionnaire de contexte doit-il être annoté avec des indications de type Python?
import typing
@contextlib.contextmanager
def foo() -> ???:
yield
Le documentation sur contextlib ne mentionne pas beaucoup les types.
La documentation sur typing.ContextManager n'est pas du tout utile non plus.
Il y a aussi typing.Generator , qui a au moins un exemple. Cela signifie-t-il que je devrais utiliser typing.Generator[None, None, None]
et pas typing.ContextManager
?
import typing
@contextlib.contextmanager
def foo() -> typing.Generator[None, None, None]:
yield
Chaque fois que je ne suis pas sûr à 100% des types acceptés par une fonction, j'aime consulter typeshed , qui est le référentiel canonique des indices de type pour Python. Mypy regroupe directement et utilise le typage pour l'aider à effectuer sa vérification de type, par exemple.
Nous pouvons trouver les talons pour contextlib ici: 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]]: ...
C'est un peu écrasant, mais la ligne qui nous intéresse est celle-ci:
def contextmanager(func: Callable[..., Iterator[_T]]) -> Callable[..., ContextManager[_T]]: ...
Il indique que le décorateur prend un Callable[..., Iterator[_T]]
- une fonction avec des arguments arbitraires renvoyant un itérateur. Donc en conclusion, ce serait bien de faire:
@contextlib.contextmanager
def foo() -> Iterator[None]:
yield
Alors, pourquoi utiliser Generator[None, None, None]
fonctionne aussi, comme le suggèrent les commentaires?
C'est parce que Generator
est un sous-type de Iterator
- nous pouvons à nouveau vérifier cela par nous-mêmes en consultant typedhed . Donc, si notre fonction retourne un générateur, il est toujours compatible avec ce que contextmanager
attend donc mypy l'accepte sans problème.
La version Iterator[]
Ne fonctionne pas lorsque vous souhaitez renvoyer la référence du gestionnaire de contexte. Par exemple, le code suivant:
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]:
...
Produira une erreur sur la ligne return assert_timing(high=seconds)
:
Incompatible return value type (got "_GeneratorContextManager[None]", expected "Iterator[None]")
Toute utilisation légitime de la fonction:
with assert_faster_than(1):
be_quick()
Entraînera quelque chose comme ceci:
"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__"?
Vous pouvez le réparer comme ça ...
def assert_faster_than(...) -> Iterator[None]:
with assert_timing(...):
yield
Mais je vais utiliser le nouvel objet ContextManager[]
À la place et faire taire mypy pour le décorateur:
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]:
...
Le type de retour de la fonction encapsulée par un gestionnaire de contexte est Iterator[None]
.
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def foo() -> Iterator[None]:
yield