9 min de leitura
#10 - Argumentos Avançados e Type Hints
Como forçar argumentos somente-posicionais ou somente-nomeados numa função Python, e como usar type hints e anotações de função para documentar os tipos esperados.
No post anterior você viu argumentos posicionais, nomeados, valores padrão, *args e **kwargs — o suficiente pra escrever praticamente qualquer função do dia a dia. Só que, conforme uma função cresce ou passa a ser usada por outras pessoas do seu time, dois problemas aparecem: às vezes você quer forçar um jeito específico de passar um argumento (só por posição, ou só por nome), e quase sempre você quer deixar claro que tipo de dado cada parâmetro espera, sem precisar escrever isso só nos comentários.
Neste post você vai ver como resolver os dois problemas: argumentos somente-posicionais e somente-nomeados, e type hints.
Argumentos somente-posicionais
Colocando uma / na lista de parâmetros, tudo que vem antes dela só pode ser passado por posição — tentar usar o nome do parâmetro nessa chamada dá erro.
def dividir(a, b, /):
return a / b
print(dividir(10, 2))
A saída é:
5.0
Se você tentar chamar dividir(a=10, b=2), o Python lança um TypeError, porque a e b foram declarados como somente-posicionais. Isso é útil quando o nome do parâmetro não tem muito significado pra quem está chamando a função (a e b numa divisão, por exemplo), ou quando você quer liberdade pra renomear o parâmetro no futuro sem quebrar o código de quem já usa a função.
Argumentos somente-nomeados
O caminho inverso também existe: colocando um * na lista de parâmetros, tudo que vem depois dele só pode ser passado explicitando o nome.
def criar_usuario(nome, *, admin=False):
print(f"Usuário {nome}, admin={admin}")
criar_usuario("Ana", admin=True)
A saída é:
Usuário Ana, admin=True
Tentar chamar criar_usuario("Ana", True) (passando admin por posição) dá erro. Isso é especialmente útil pra parâmetros booleanos ou pouco óbvios: criar_usuario("Ana", True) não deixa claro o que aquele True representa só de olhar a chamada, enquanto criar_usuario("Ana", admin=True) é auto-explicativo.
Combinando os dois
Dá pra misturar as três categorias numa função só: primeiro os parâmetros somente-posicionais, depois os “normais” (que aceitam qualquer um dos dois jeitos), depois os somente-nomeados.
def registrar(nome, idade, /, cidade, *, ativo=True):
print(f"{nome}, {idade} anos, mora em {cidade}, ativo={ativo}")
registrar("Bruno", 30, cidade="Recife", ativo=False)
A saída é:
Bruno, 30 anos, mora em Recife, ativo=False
Aqui, nome e idade só podem vir por posição, cidade aceita qualquer jeito, e ativo só pode vir por nome.
Nota: você não precisa usar
/e*em toda função que escrever — a maioria do código Python do dia a dia nem usa. Eles fazem sentido quando você está projetando uma função que outras pessoas (ou você mesmo, no futuro) vão chamar bastante, e quer deixar a interface mais clara ou mais flexível de manter.
Anotações de função
Function annotations são um jeito de anexar informação extra a um parâmetro ou ao retorno de uma função, usando : depois do nome do parâmetro e -> antes dos dois-pontos finais da definição. O uso mais comum, de longe, é indicar o tipo esperado.
def saudacao(nome: str, idade: int) -> str:
return f"{nome} tem {idade} anos"
print(saudacao("Carla", 27))
A saída é:
Carla tem 27 anos
O Python guarda essas anotações num dicionário especial, __annotations__, que você pode inspecionar em tempo de execução.
def saudacao(nome: str, idade: int) -> str:
return f"{nome} tem {idade} anos"
print(saudacao.__annotations__)
A saída é:
{'nome': <class 'str'>, 'idade': <class 'int'>, 'return': <class 'str'>}
Type hints
Type hints é o nome mais comum pra esse mesmo recurso quando usado especificamente pra indicar tipos — e é praticamente onipresente em código Python profissional hoje em dia. Além dos tipos básicos (str, int, float, bool), dá pra anotar estruturas mais elaboradas, como listas e dicionários com um tipo específico de conteúdo.
def media(numeros: list[float]) -> float:
return sum(numeros) / len(numeros)
print(media([8.5, 9.0, 7.5]))
A saída é:
8.333333333333334
list[float] diz “uma lista onde cada item é um float”. O mesmo padrão vale pra dict[str, int] (dicionário com chaves texto e valores inteiros), tuple[int, int] (tupla com dois inteiros) e assim por diante.
Quando um valor pode ser de um tipo ou None, a forma mais moderna de indicar isso é com |:
def buscar_usuario(id: int) -> str | None:
usuarios = {1: "Ana", 2: "Bruno"}
return usuarios.get(id)
print(buscar_usuario(1))
print(buscar_usuario(99))
A saída é:
Ana
None
Nota: essa é provavelmente a maior pegadinha de type hints em Python: eles são só documentação pro interpretador. Nada impede você de chamar
saudacao(123, "trinta")com os tipos trocados — o Python não vai reclamar em tempo de execução, e o programa só vai quebrar (ou pior, dar um resultado errado silenciosamente) se o código tentar tratar123como texto. Quem de fato verifica se os tipos batem são ferramentas externas como omypy, rodadas separadamente, geralmente como parte do processo de CI de um projeto.
Mesmo sem checagem automática, type hints valem a pena: deixam a assinatura da função auto-documentada, ajudam o autocomplete do seu editor a ser muito mais preciso, e facilitam bastante entender o que uma função espera sem precisar ler o corpo inteiro dela.
A pegadinha dos valores padrão mutáveis
Já que o assunto é valor padrão de argumento, tem uma armadilha clássica do Python que vale conhecer antes de sair usando valores padrão livremente: nunca use uma lista, dicionário ou outro objeto mutável como valor padrão.
def adicionar_item(item, lista=[]):
lista.append(item)
return lista
print(adicionar_item("maçã"))
print(adicionar_item("banana"))
A saída (bem contraintuitiva) é:
['maçã']
['maçã', 'banana']
Repare que a segunda chamada “lembrou” do item da primeira, mesmo sem você ter passado nenhuma lista. O motivo é que o valor padrão de um parâmetro é criado uma única vez, quando a função é definida — não a cada chamada. Todas as chamadas que não passam lista explicitamente acabam compartilhando exatamente a mesma lista por baixo dos panos.
O jeito correto de resolver isso é usar None como padrão, e criar a lista nova dentro do corpo da função.
def adicionar_item(item, lista=None):
if lista is None:
lista = []
lista.append(item)
return lista
print(adicionar_item("maçã"))
print(adicionar_item("banana"))
A saída é:
['maçã']
['banana']
Nota: essa pegadinha vale pra qualquer valor mutável usado como padrão (listas, dicionários, sets, ou instâncias de classes que você mesmo escreveu). Valores imutáveis como padrão — números, strings, tuplas,
None— não têm esse problema, porque não têm como ser alterados “no lugar” por uma chamada anterior.
Com argumentos somente-posicionais, somente-nomeados e type hints, suas funções ficam com uma interface mais clara tanto pra quem chama quanto pra quem lê o código depois. No próximo post, você vai ver como funções podem ser tratadas como qualquer outro valor em Python — passadas de um lado pro outro, devolvidas por outras funções — com funções de alta ordem e closures.
Fonte adaptada: Python Positional-Only Arguments, Python Keyword-Only Arguments, Python Function Annotations, Python Type Hints
Comentários