Funções são blocos fundamentais em Python, e a forma como passamos argumentos para elas é crucial para escrever código flexível e legível. Nesta aula, vamos explorar os diferentes mecanismos de passagem de argumentos: argumentos posicionais, nomeados, valores padrão, coleta de argumentos variáveis com *args e **kwargs, e argumentos keyword-only. Ao final, você será capaz de projetar funções que se adaptam a diferentes necessidades sem sacrificar a clareza.

Dominar esses conceitos é essencial para trabalhar com bibliotecas, criar APIs elegantes e evitar erros comuns. Vamos começar!

Posicionais e nomeados

Em Python, ao chamar uma função, podemos passar argumentos de duas formas: posicionalmente (na ordem dos parâmetros) ou por nome (keyword). Argumentos posicionais são mapeados aos parâmetros pela ordem em que são fornecidos. Já os argumentos nomeados são associados explicitamente a um parâmetro, independentemente da ordem.

Exemplo:

def saudacao(nome, mensagem):
    print(f"{mensagem}, {nome}!")

# Chamada posicional
saudacao("Alice", "Olá")  # Saída: Olá, Alice!

# Chamada nomeada
saudacao(mensagem="Oi", nome="Bob")  # Saída: Oi, Bob!

É possível misturar argumentos posicionais e nomeados, mas todos os argumentos posicionais devem vir antes dos nomeados. Caso contrário, ocorrerá um erro de sintaxe.

Valores padrão

Parâmetros podem ter valores padrão, tornando-os opcionais na chamada. Se o argumento não for fornecido, o valor padrão é usado. É importante lembrar que os valores padrão são avaliados apenas uma vez, no momento da definição da função, e não a cada chamada.

Exemplo:

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

saudacao("Alice")          # Olá, Alice!
saudacao("Bob", "Oi")     # Oi, Bob!

Cuidado com valores mutáveis como padrão, pois eles são compartilhados entre chamadas:

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

print(adiciona_item(1))  # [1]
print(adiciona_item(2))  # [1, 2]  (compartilha a mesma lista!)

Para evitar isso, use None e crie um novo objeto 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 operador *args coleta argumentos posicionais extras em uma tupla, enquanto **kwargs coleta argumentos nomeados extras em um dicionário. Os nomes args e kwargs são convenções, mas os asteriscos são obrigatórios.

Exemplo:

def func(a, b, *args, **kwargs):
    print(f"a = {a}, b = {b}")
    print(f"args = {args}")
    print(f"kwargs = {kwargs}")

func(1, 2, 3, 4, x=5, y=6)
# Saída:
# a = 1, b = 2
# args = (3, 4)
# kwargs = {'x': 5, 'y': 6}

Essa técnica é muito usada em wrappers, decoradores e funções que precisam repassar argumentos para outras funções.

Argumentos keyword-only

Argumentos keyword-only são aqueles que só podem ser passados por nome, nunca posicionalmente. Eles vêm depois de * (ou depois de *args). Isso força o chamador a ser explícito, melhorando a legibilidade.

Exemplo:

def func(a, b, *, opcao1, opcao2):
    print(f"a={a}, b={b}, opcao1={opcao1}, opcao2={opcao2}")

func(1, 2, opcao1="sim", opcao2="não")  # Válido
func(1, 2, "sim", "não")  # Erro! TypeError: func() takes 2 positional arguments but 4 were given

Também é possível ter argumentos keyword-only com valores padrão, tornando-os opcionais:

def func(a, b, *, opcao1="padrão", opcao2=None):
    pass

Isso é útil para parâmetros de configuração ou flags que tornam a chamada mais clara.

Boas práticas e observações finais

Use argumentos posicionais para parâmetros obrigatórios e óbvios pela ordem. Prefira argumentos nomeados para parâmetros opcionais ou quando muitos parâmetros podem tornar a ordem confusa. Valores padrão devem ser imutáveis para evitar comportamentos inesperados. *args e **kwargs são poderosos, mas use com moderação para não esconder a assinatura da função. Argumentos keyword-only são ótimos para forçar clareza em APIs públicas.

Referências

Exercícios

  1. Exercício 1: Defina uma função cumprimentar que aceita um argumento posicional nome e um argumento nomeado saudacao com valor padrão "Olá". A função deve imprimir a saudação. Teste com e sem o argumento nomeado.
  2. ✓ Resposta:
    def cumprimentar(nome, saudacao="Olá"):
        print(f"{saudacao}, {nome}!")
    
    cumprimentar("Ana")
    cumprimentar("Pedro", saudacao="Oi")
    
  3. Exercício 2: Crie uma função soma que aceita um número variável de argumentos posicionais (usando *args) e retorna a soma de todos eles. Teste com diferentes quantidades de números.
  4. ✓ Resposta:
    def soma(*args):
        return sum(args)
    
    print(soma(1, 2, 3))
    print(soma(10, 20))
    print(soma())
    
  5. Exercício 3: Escreva uma função monta_dicionario que aceita argumentos nomeados variáveis (**kwargs) e retorna um dicionário com esses pares chave-valor. Teste com alguns argumentos.
  6. ✓ Resposta:
    def monta_dicionario(**kwargs):
        return kwargs
    
    print(monta_dicionario(nome="Alice", idade=30, cidade="São Paulo"))
    
  7. Exercício 4: Defina uma função configurar que recebe dois parâmetros posicionais (host e porta) e dois parâmetros keyword-only (ssl e timeout) com valores padrão True e 30, respectivamente. A função deve imprimir todos os parâmetros. Chame a função de duas maneiras: uma passando os keyword-only e outra omitindo.
  8. ✓ Resposta:
    def configurar(host, porta, *, ssl=True, timeout=30):
        print(f"host={host}, porta={porta}, ssl={ssl}, timeout={timeout}")
    
    configurar("localhost", 8080, ssl=False, timeout=60)
    configurar("127.0.0.1", 8000)
    
  9. Exercício 5: Crie uma função calculadora que aceita um operador (string) como primeiro argumento posicional, seguido de *args para números. Dependendo do operador ('+', '-', '*', '/'), realize a operação correspondente sobre todos os números (pelo menos dois). Use sum para soma, reduce para subtração etc. (importe functools.reduce). Teste com diferentes operadores.
  10. ✓ Resposta:
    from functools import reduce
    
    def calculadora(operador, *args):
        if operador == '+':
            return sum(args)
        elif operador == '-':
            return reduce(lambda x, y: x - y, args)
        elif operador == '*':
            return reduce(lambda x, y: x * y, args)
        elif operador == '/':
            return reduce(lambda x, y: x / y, args)
        else:
            raise ValueError("Operador inválido")
    
    print(calculadora('+', 1, 2, 3))
    print(calculadora('-', 10, 2, 3))
    print(calculadora('*', 2, 3, 4))
    print(calculadora('/', 100, 2, 5))