Escriba anotaciones para * args y ** kwargs


158

Estoy probando las anotaciones de tipo de Python con clases base abstractas para escribir algunas interfaces. ¿Hay alguna manera de anotar los posibles tipos de *argsy **kwargs?

Por ejemplo, ¿cómo se podría expresar que los argumentos razonables para una función son una into dos ints? type(args)da, Tupleasí que mi suposición fue anotar el tipo como Union[Tuple[int, int], Tuple[int]], pero esto no funciona.

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))

Mensajes de error 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]]"

Tiene sentido que a mypy no le guste esto para la llamada a la función porque espera que haya una tupleen la llamada misma. La adición después de desempacar también da un error de tipeo que no entiendo.

¿Cómo se anotan los tipos sensibles para *argsy **kwargs?

Respuestas:


167

Para argumentos posicionales variables ( *args) y argumentos de palabras clave variables ( **kw) solo necesita especificar el valor esperado para uno de esos argumentos.

Desde las listas de argumentos arbitrarios y la sección de valores de argumentos predeterminados de la sugerencia de tipo PEP:

Las listas de argumentos arbitrarios también se pueden anotar, de modo que la definición:

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

es aceptable y significa que, por ejemplo, todo lo siguiente representa llamadas a funciones con tipos válidos de argumentos:

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

Por lo tanto, desea especificar su método de esta manera:

def foo(*args: int):

Sin embargo, si su función solo puede aceptar uno o dos valores enteros, no debe usar *argsen absoluto, use un argumento de posición explícito y un segundo argumento de palabra clave:

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

Ahora su función está realmente limitada a uno o dos argumentos, y ambos deben ser enteros si se especifica. *args siempre significa 0 o más, y no puede limitarse por sugerencias de tipo a un rango más específico.


1
Por curiosidad, ¿por qué agregar el Optional? ¿Cambió algo sobre Python o cambiaste de opinión? ¿Todavía no es estrictamente necesario debido al Nonevalor predeterminado?
Praxeolítico

10
@Praxeolitic sí, en la práctica, la Optionalanotación implícita automática cuando se usa Nonecomo valor predeterminado hizo que ciertos casos de uso fueran más difíciles y ahora se está eliminando de la PEP.
Martijn Pieters

55
Aquí hay un enlace que discute esto para aquellos interesados. Ciertamente, parece Optionalque se requerirá explícitamente en el futuro.
Rick apoya a Mónica

En realidad, esto no es compatible con Callable: github.com/python/mypy/issues/5876
Shital Shah

1
@ShitalShah: ese no es realmente el problema. Callableno admite ninguna mención de una sugerencia de tipo *argso punto **kwargs final . Ese problema específico se trata de marcar incrustaciones que aceptan argumentos específicos más un número arbitrario de otros y, por lo tanto *args: Any, **kwargs: Any, usan una sugerencia de tipo muy específica para los dos términos generales. Para los casos en los que establece *argsy / o **kwargsalgo más específico, puede usar a Protocol.
Martijn Pieters

26

La forma correcta de hacer esto es usar @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))

Tenga en cuenta que no agrega @overloadni escribe anotaciones a la implementación real, que debe ser la última.

Necesitará una versión más reciente de ambos typingy mypy para obtener soporte para @overload fuera de los archivos stub .

También puede usar esto para variar el resultado devuelto de manera que haga explícito qué tipos de argumento corresponden con qué tipo de retorno. p.ej:

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))

2
Me gusta esta respuesta porque aborda el caso más general. Mirando hacia atrás, no debería haber usado (type1)vs (type1, type1)llamadas a funciones como mi ejemplo. Quizás (type1)vs (type2, type1)hubiera sido un mejor ejemplo y muestra por qué me gusta esta respuesta. Esto también permite diferentes tipos de retorno. Sin embargo, en el caso especial en el que solo tiene un tipo de retorno y su *argsy *kwargstodos son del mismo tipo, la técnica en la respuesta de Martjin tiene más sentido, por lo que ambas respuestas son útiles.
Praxeolítico

44
Sin embargo, usar *argsdonde hay un número máximo de argumentos (2 aquí) sigue siendo incorrecto .
Martijn Pieters

1
@MartijnPieters ¿Por qué está *argsnecesariamente mal aquí? Si las llamadas esperadas eran (type1)vs (type2, type1), entonces el número de argumentos es variable y no hay un valor predeterminado apropiado para el argumento final. ¿Por qué es importante que haya un máximo?
Praxeolítico

1
*argsestá realmente allí para cero o más argumentos sin límites, homogéneos, o para "pasarlos por todos". Tiene un argumento requerido y uno opcional. Eso es totalmente diferente y normalmente se maneja dando al segundo argumento un valor predeterminado de centinela para detectar que se omitió.
Martijn Pieters

3
Después de mirar el PEP, este claramente no es el uso previsto de @overload. Si bien esta respuesta muestra una forma interesante de anotar individualmente los tipos de *args, una respuesta aún mejor a la pregunta es que esto no es algo que deba hacerse en absoluto.
Praxeolítico

20

Como una breve adición a la respuesta anterior, si está tratando de usar mypy en archivos de Python 2 y necesita usar comentarios para agregar tipos en lugar de anotaciones, debe prefijar los tipos para argsy kwargscon *y **respectivamente:

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

Mypy trata esto como si fuera la misma versión de Python 3.5 de abajo foo:

def foo(param: bool, *args: str, **kwargs: int) -> None:
    pass
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.