web-dev-qa-db-fra.com

Python conseils de type et gestionnaires de contexte

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
31
Peter

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.

8
Michael0x2a

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

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
1
David Foster