Anotações de tipo para * args e ** kwargs

158

Estou tentando anotações de tipo do Python com classes base abstratas para escrever algumas interfaces. Existe uma maneira de anotar os possíveis tipos de *argse **kwargs?

Por exemplo, como alguém expressaria que os argumentos sensíveis a uma função são um intou dois ints? type(args)dá, Tupleentão meu palpite era anotar o tipo como Union[Tuple[int, int], Tuple[int]], mas isso não 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))

Mensagens de erro 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]]"

Faz sentido que mypy não goste disso na chamada de função porque espera que exista um tuplena própria chamada. A adição após a descompactação também gera um erro de digitação que eu não entendo.

Como se anota os tipos sensíveis para *argse **kwargs?

Praxeolitic
fonte

Respostas:

167

Para argumentos posicionais variáveis ​​( *args) e argumentos de palavras-chave variáveis ​​( **kw), você só precisa especificar o valor esperado para um desses argumentos.

Na lista de argumentos arbitrários e na seção de valores padrão dos argumentos do PEP de dicas de tipo :

As listas de argumentos arbitrárias também podem ser anotadas, para que a definição:

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

é aceitável e significa que, por exemplo, todos os itens a seguir representam chamadas de função com tipos válidos de argumentos:

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

Então, você deseja especificar seu método assim:

def foo(*args: int):

No entanto, se sua função puder aceitar apenas um ou dois valores inteiros, você não deve usar *args, use um argumento posicional explícito e um segundo argumento de palavra-chave:

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

Agora, sua função está realmente limitada a um ou dois argumentos e ambos devem ser inteiros, se especificados. *args sempre significa 0 ou mais e não pode ser limitado pelas dicas de tipo a um intervalo mais específico.

Martijn Pieters
fonte
1
Apenas curioso, por que adicionar o Optional? Algo mudou no Python ou você mudou de idéia? Ainda não é estritamente necessário devido ao Nonepadrão?
Praxeolitic 25/08
10
@Praxeolitic sim, na prática, a Optionalanotação automática implícita quando você usa Nonecomo valor padrão tornou certos casos de uso mais difíceis e que agora estão sendo removidos do PEP.
Martijn Pieters
5
Aqui está um link discutindo isso para os interessados. Certamente parece explícito Optionalque será necessário no futuro.
Rick suporta Monica
Na verdade, isso não é suportado pelo Callable: github.com/python/mypy/issues/5876
Shital Shah
1
@ShitalShah: não é exatamente sobre isso que se trata. Callablenão suporta qualquer menção de uma dica de tipo para *argsou **kwargs ponto final . Essa questão específica é sobre a marcação de chamadas que aceitam argumentos específicos mais um número arbitrário de outras pessoas e, portanto *args: Any, **kwargs: Any, usam uma dica de tipo muito específica para os dois exemplos. Nos casos em que você define *argse / ou **kwargsalgo mais específico, você pode usar a Protocol.
Martijn Pieters
26

A maneira correta de fazer isso é usando @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))

Observe que você não adiciona @overloadou digita anotações na implementação real, que deve vir por último.

Você precisará de uma versão nova typinge de mypy para obter suporte para @overload fora dos arquivos stub .

Você também pode usar isso para variar o resultado retornado de uma maneira que explique quais tipos de argumento correspondem a qual tipo de retorno. por exemplo:

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
fonte
2
Eu gosto desta resposta porque aborda o caso mais geral. Olhando para trás, eu não deveria ter usado chamadas de função (type1)vs (type1, type1)como meu exemplo. Talvez (type1)vs (type2, type1)teria sido um exemplo melhor e mostra por que eu gosto dessa resposta. Isso também permite diferentes tipos de retorno. No entanto, no caso especial em que você só tem um tipo de retorno e seu *argse *kwargssão todos do mesmo tipo, a técnica na resposta de Martjin faz mais sentido, portanto as duas respostas são úteis.
Praxeolitic 25/08
4
Usar *argsonde há um número máximo de argumentos (2 aqui) ainda está errado .
Martijn Pieters
1
@MartijnPieters Por que está *argsnecessariamente errado aqui? Se as chamadas esperadas foram (type1)vs (type2, type1), o número de argumentos é variável e não há um padrão apropriado para o argumento à direita. Por que é importante que haja um máximo?
Praxeolitic
1
*argsexiste realmente para zero ou mais argumentos não limitados e homogêneos ou para "transmitir esses argumentos intactos". Você tem um argumento necessário e um opcional. Isso é totalmente diferente e normalmente é tratado, atribuindo ao segundo argumento um valor padrão sentinela para detectar que foi omitido.
Martijn Pieters
3
Depois de analisar o PEP, esse claramente não é o uso pretendido do @overload. Embora esta resposta mostre uma maneira interessante de anotar individualmente os tipos *args, uma resposta ainda melhor para a pergunta é que isso não é algo que deva ser feito.
Praxeolitic 25/08
20

Como uma pequena adição à resposta anterior, se você estiver tentando usar o mypy em arquivos Python 2 e precisar usar comentários para adicionar tipos em vez de anotações, precisará prefixar os tipos para argse kwargscom *e **respectivamente:

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

Isso é tratado por mypy como sendo o mesmo da versão Python 3.5 abaixo foo:

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