Funções são blocos fundamentais na programação Python, e a forma como passamos argumentos para elas influencia diretamente a flexibilidade e clareza do código. Nesta aula, vamos mergulhar nos diferentes mecanismos de passagem de argumentos: posicionais, nomeados, valores padrão, uso de *args e **kwargs, e argumentos keyword-only. Dominar esses conceitos permite escrever funções que se adaptam a diversas situações sem sacrificar a legibilidade.

Vamos começar com os fundamentos e, em seguida, explorar padrões mais avançados que tornam as funções Python tão poderosas. Ao final, você será capaz de projetar interfaces de função claras e robustas.

Posicionais e nomeados

Em Python, os argumentos podem ser passados de duas formas principais: posicionais (por ordem) ou nomeados (por nome). Argumentos posicionais são aqueles cujo valor é atribuído com base na posição em que são fornecidos na chamada da função. Já os argumentos nomeados são passados explicitamente com o nome do parâmetro, independentemente da ordem.

Considere uma função simples que calcula a potência de um número:

def potencia(base, expoente):
    return base ** expoente

# Chamada posicional
print(potencia(2, 3))  # 8

# Chamada nomeada
print(potencia(expoente=3, base=2))  # 8

Na chamada posicional, 2 é atribuído a base e 3 a expoente pela ordem. Na chamada nomeada, a ordem não importa, pois os valores são vinculados pelos nomes dos parâmetros. É possível misturar ambos, mas os argumentos posicionais devem vir antes dos nomeados:

print(potencia(2, expoente=3))  # Válido
# print(potencia(base=2, 3))    # Inválido! SyntaxError

Valores padrão

Parâmetros com valores padrão permitem que a função seja chamada sem fornecer todos os argumentos. O valor padrão é usado se o argumento correspondente não for passado. Por exemplo:

def saudacao(nome, saudacao="Olá"):
    return f"{saudacao}, {nome}!"

print(saudacao("João"))          # Olá, João!
print(saudacao("Maria", "Oi"))   # Oi, Maria!

Atenção: valores padrão são avaliados apenas uma vez, no momento da definição da função. Isso é importante quando o valor padrão é mutável, como listas ou dicionários. Exemplo clássico de problema:

def adiciona_item(item, lista=[]):
    lista.append(item)
    return lista

print(adiciona_item(1))  # [1]
print(adiciona_item(2))  # [1, 2]  # Surpresa! A lista padrão foi modificada

Para evitar esse comportamento, use None como valor padrão e crie uma nova lista dentro da função:

def adiciona_item(item, lista=None):
    if lista is None:
        lista = []
    lista.append(item)
    return lista

*args e **kwargs

Às vezes, queremos que uma função aceite um número variável de argumentos. O *args permite passar um número arbitrário de argumentos posicionais, que são empacotados em uma tupla. Já **kwargs captura argumentos nomeados extras em um dicionário. Exemplo:

def print_args(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)

print_args(1, 2, 3, nome="Ana", idade=30)
# args: (1, 2, 3)
# kwargs: {'nome': 'Ana', 'idade': 30}

Os nomes args e kwargs são convenções; você pode usar qualquer nome, mas o asterisco único (*) e duplo (**) são a parte essencial. Esses parâmetros devem vir após todos os parâmetros posicionais e antes dos keyword-only (se houver).

Um uso comum é delegar argumentos para outra função:

def chamar_com_log(func, *args, **kwargs):
    print(f"Chamando {func.__name__} com args={args} e kwargs={kwargs}")
    return func(*args, **kwargs)

print(chamar_com_log(potencia, 2, 3))  # Chamando potencia com args=(2,3) e kwargs={} -> 8

Argumentos keyword-only

Argumentos keyword-only são aqueles que só podem ser passados por nome (não posicionalmente). Eles são definidos após um * solitário na lista de parâmetros. Por exemplo:

def configurar(host, port, *, timeout=30, verbose=False):
    print(f"Conectando a {host}:{port} (timeout={timeout}, verbose={verbose})")

# Válido:
configurar("localhost", 8080, timeout=10, verbose=True)
# Inválido:
# configurar("localhost", 8080, 10, True)  # TypeError: takes 2 positional arguments but 4 were given

Os parâmetros após o * são keyword-only. Eles podem ter valores padrão ou não. Se não tiverem, devem ser fornecidos obrigatoriamente por nome. Isso torna a chamada mais explícita e evita erros de posição.

Você também pode combinar *args com keyword-only. Nesse caso, os keyword-only vêm depois de *args:

def func(a, b, *args, c, d=10):
    print(a, b, args, c, d)

func(1, 2, 3, 4, c=5)  # 1 2 (3,4) 5 10

Observe que c é obrigatório e deve ser passado por nome. Isso é útil para APIs onde alguns parâmetros são essenciais e devem ser explicitados.

Boas práticas

  • Prefira argumentos nomeados para parâmetros booleanos ou opcionais, aumentando a legibilidade.
  • Evite valores padrão mutáveis; use None e crie novos objetos dentro da função.
  • Use *args e **kwargs principalmente em funções wrapper ou quando a interface precisa ser flexível, mas documente bem os parâmetros esperados.
  • Argumentos keyword-only são excelentes para parâmetros de configuração que não devem ser confundidos com dados posicionais.
  • Mantenha a ordem correta: parâmetros posicionais, *args, keyword-only (com ou sem padrão), **kwargs.

Referências

Exercícios

  1. Crie uma função chamada calcular que aceite dois números e uma operação (string) como argumento posicional, e a operação deve ter valor padrão "soma". A função deve retornar o resultado da operação (soma, subtração, multiplicação, divisão).

    ✓ Resposta:
    def calcular(a, b, operacao="soma"):
        if operacao == "soma":
            return a + b
        elif operacao == "subtracao":
            return a - b
        elif operacao == "multiplicacao":
            return a * b
        elif operacao == "divisao":
            if b != 0:
                return a / b
            else:
                return "Erro: divisão por zero"
        else:
            return "Operação inválida"
    
    # Teste
    print(calcular(10, 5))          # 15
    print(calcular(10, 5, "multiplicacao"))  # 50
    
  2. Escreva uma função concatenar que aceite uma string separadora e um número arbitrário de strings via *args, e retorne a concatenação de todas as strings usando o separador.

    ✓ Resposta:
    def concatenar(separador, *args):
        return separador.join(args)
    
    print(concatenar("-", "a", "b", "c"))  # a-b-c
    print(concatenar(", ", "Python", "Java", "C"))  # Python, Java, C
    
  3. Crie uma função criar_perfil que aceite nome e idade como posicionais, e depois aceite argumentos nomeados arbitrários (**kwargs) representando outros dados (ex: cidade, profissao). A função deve retornar um dicionário com todos os dados.

    ✓ Resposta:
    def criar_perfil(nome, idade, **kwargs):
        perfil = {"nome": nome, "idade": idade}
        perfil.update(kwargs)
        return perfil
    
    print(criar_perfil("Ana", 30, cidade="São Paulo", profissao="Engenheira"))
    # {'nome': 'Ana', 'idade': 30, 'cidade': 'São Paulo', 'profissao': 'Engenheira'}
    
  4. Defina uma função conectar que exija dois argumentos posicionais (host, port) e tenha dois argumentos keyword-only: timeout (padrão 30) e ssl (padrão False). A função deve imprimir uma mensagem com os parâmetros.

    ✓ Resposta:
    def conectar(host, port, *, timeout=30, ssl=False):
        print(f"Conectando a {host}:{port} (timeout={timeout}, ssl={ssl})")
    
    conectar("localhost", 8080)
    conectar("example.com", 443, timeout=10, ssl=True)
    # Tentativa inválida: conectar("localhost", 8080, 20)  # TypeError
    
  5. Escreva uma função decorar que aceite uma função como argumento e retorne uma nova função que imprime "Antes" antes de chamar a função original e "Depois" depois. Use *args e **kwargs para repassar argumentos.

    ✓ Resposta:
    def decorar(func):
        def wrapper(*args, **kwargs):
            print("Antes")
            resultado = func(*args, **kwargs)
            print("Depois")
            return resultado
        return wrapper
    
    @decorar
    def saudacao(nome):
        print(f"Olá, {nome}!")
    
    saudacao("João")
    # Antes
    # Olá, João!
    # Depois