Tapez des annotations pour * args et ** kwargs

159

J'essaie les annotations de type Python avec des classes de base abstraites pour écrire certaines interfaces. Existe-t-il un moyen d'annoter les types possibles de *argset **kwargs?

Par exemple, comment exprimerait-on que les arguments sensibles d'une fonction sont soit un intou deux ints? type(args)donne Tupledonc ma supposition était d'annoter le type comme Union[Tuple[int, int], Tuple[int]], mais cela ne fonctionne pas.

from typing import Union, Tuple

def foo(*args: Union[Tuple[int, int], Tuple[int]]):
    try:
        i, j = args
        return i + j
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

# ok
print(foo((1,)))
print(foo((1, 2)))
# mypy does not like this
print(foo(1))
print(foo(1, 2))

Messages d'erreur de mypy:

t.py: note: In function "foo":
t.py:6: error: Unsupported operand types for + ("tuple" and "Union[Tuple[int, int], Tuple[int]]")
t.py: note: At top level:
t.py:12: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:14: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:15: error: Argument 1 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"
t.py:15: error: Argument 2 to "foo" has incompatible type "int"; expected "Union[Tuple[int, int], Tuple[int]]"

Il est logique que mypy n'aime pas cela pour l'appel de fonction car il s'attend à ce qu'il y ait un tupledans l'appel lui-même. L'ajout après le déballage donne également une erreur de frappe que je ne comprends pas.

Comment annoter les types sensibles pour *argset **kwargs?

Praxéolitique
la source

Réponses:

168

Pour les arguments de position variables ( *args) et les arguments de mots-clés variables ( **kw), il vous suffit de spécifier la valeur attendue pour un de ces arguments.

A partir de la liste d'arguments arbitraires et les valeurs d'argument par défaut section du type Conseils PEP:

Les listes d'arguments arbitraires peuvent également être annotées de type, de sorte que la définition:

def foo(*args: str, **kwds: int): ...

est acceptable et cela signifie que, par exemple, tous les éléments suivants représentent des appels de fonction avec des types d'arguments valides:

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

Vous voudrez donc spécifier votre méthode comme ceci:

def foo(*args: int):

Cependant, si votre fonction ne peut accepter qu'une ou deux valeurs entières, vous ne devez pas utiliser *argsdu tout, utilisez un argument de position explicite et un deuxième argument de mot-clé:

def foo(first: int, second: Optional[int] = None):

Maintenant, votre fonction est en fait limitée à un ou deux arguments, et les deux doivent être des entiers si spécifié. signifie *args toujours 0 ou plus, et ne peut pas être limité par des indices de type à une plage plus spécifique.

Martijn Pieters
la source
1
Juste curieux, pourquoi ajouter le Optional? Quelque chose a changé à propos de Python ou avez-vous changé d'avis? N'est-il toujours pas strictement nécessaire en raison du Nonedéfaut?
Praxeolitic
10
@Praxeolitic oui, en pratique, l' Optionalannotation implicite automatique lorsque vous utilisez Nonecomme valeur par défaut a rendu certains cas d'utilisation plus difficiles et qui est maintenant supprimée du PEP.
Martijn Pieters
5
Voici un lien discutant de cela pour les personnes intéressées. Cela semble certainement être explicite Optionaldans le futur.
Rick soutient Monica le
Ce n'est en fait pas pris en charge pour Callable
Shital Shah
1
@ShitalShah: ce n'est pas vraiment le sujet de ce problème. Callablene prend en charge aucune mention d'un indice de type *argsou d' **kwargs un point . Ce problème spécifique concerne le marquage des callables qui acceptent des arguments spécifiques plus un nombre arbitraire d'autres , et donc utilisent *args: Any, **kwargs: Any, un indice de type très spécifique pour les deux fourre-tout. Pour les cas où vous définissez *argset / ou **kwargsquelque chose de plus spécifique, vous pouvez utiliser un Protocol.
Martijn Pieters
26

La bonne façon de faire est d'utiliser @overload

from typing import overload

@overload
def foo(arg1: int, arg2: int) -> int:
    ...

@overload
def foo(arg: int) -> int:
    ...

def foo(*args):
    try:
        i, j = args
        return i + j
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

print(foo(1))
print(foo(1, 2))

Notez que vous n'ajoutez @overloadni ne tapez d'annotations à l'implémentation réelle, qui doit venir en dernier.

Vous aurez besoin d'une nouvelle version des deux typinget de mypy pour obtenir la prise en charge de @overload en dehors des fichiers stub .

Vous pouvez également l'utiliser pour faire varier le résultat renvoyé de manière à rendre explicite les types d'argument correspondant à quel type de retour. par exemple:

from typing import Tuple, overload

@overload
def foo(arg1: int, arg2: int) -> Tuple[int, int]:
    ...

@overload
def foo(arg: int) -> int:
    ...

def foo(*args):
    try:
        i, j = args
        return j, i
    except ValueError:
        assert len(args) == 1
        i = args[0]
        return i

print(foo(1))
print(foo(1, 2))
Chadrik
la source
2
J'aime cette réponse car elle concerne le cas le plus général. Avec le recul, je n'aurais pas dû utiliser les appels de fonction (type1)vs (type1, type1)comme exemple. Peut-être que (type1)vs (type2, type1)aurait été un meilleur exemple et montre pourquoi j'aime cette réponse. Cela permet également différents types de retour. Cependant, dans le cas particulier où vous n'avez qu'un seul type de retour et votre *argset *kwargssont tous du même type, la technique de la réponse de Martjin a plus de sens, donc les deux réponses sont utiles.
Praxeolitic
4
Utiliser *argslà où il y a un nombre maximum d'arguments (2 ici) est cependant toujours faux .
Martijn Pieters
1
@MartijnPieters Pourquoi est-ce *argsnécessairement faux ici? Si les appels attendus étaient (type1)vs (type2, type1), alors le nombre d'arguments est variable et il n'y a pas de valeur par défaut appropriée pour l'argument de fin. Pourquoi est-il important qu'il y ait un maximum?
Praxeolitic
1
*argsest vraiment là pour zéro ou plus , des arguments homogènes non plafonnés, ou pour «transmettre ces derniers intacts» fourre-tout. Vous avez un argument obligatoire et un facultatif. C'est totalement différent et est normalement géré en donnant au deuxième argument une valeur par défaut sentinelle pour détecter que ce dernier a été omis.
Martijn Pieters
3
Après avoir examiné le PEP, ce n'est clairement pas l'utilisation prévue de @overload. Bien que cette réponse montre une manière intéressante d'annoter individuellement les types de *args, une réponse encore meilleure à la question est que ce n'est pas quelque chose qui devrait être fait du tout.
Praxeolitic
20

Pour compléter brièvement la réponse précédente, si vous essayez d'utiliser mypy sur des fichiers Python 2 et que vous devez utiliser des commentaires pour ajouter des types au lieu d'annotations, vous devez préfixer les types pour argset kwargsavec *et **respectivement:

def foo(param, *args, **kwargs):
    # type: (bool, *str, **int) -> None
    pass

Ceci est traité par mypy comme étant le même que la version Python 3.5 ci-dessous de foo:

def foo(param: bool, *args: str, **kwargs: int) -> None:
    pass
Michael0x2a
la source