args e *kwargs: funções com argumentos variáveis

1. Introdução aos argumentos variáveis

Em Python, ao definir funções, frequentemente nos deparamos com situações onde o número de argumentos não é conhecido antecipadamente. Imagine criar uma função que calcula a média de notas: o professor pode ter 5 alunos em uma turma e 40 em outra. Com parâmetros fixos, seriam necessárias múltiplas definições ou soluções improvisadas.

A linguagem Python oferece uma solução elegante através dos conceitos de empacotamento (packing) e desempacotamento (unpacking) de sequências. Esses mecanismos permitem que uma função receba um número variável de argumentos, tornando o código mais flexível e reutilizável.

Os operadores *args e **kwargs são as ferramentas que possibilitam essa flexibilidade. Eles permitem capturar argumentos posicionais extras em uma tupla e argumentos nomeados extras em um dicionário, respectivamente.

2. *args: argumentos posicionais variáveis

O *args é utilizado na definição de uma função para indicar que ela pode receber um número variável de argumentos posicionais. Dentro da função, args se comporta como uma tupla contendo todos os argumentos extras fornecidos.

def soma(*args):
    total = 0
    for numero in args:
        total += numero
    return total

print(soma(1, 2, 3))        # 6
print(soma(10, 20, 30, 40)) # 100
print(soma())                # 0 (args vazio)

Observe que podemos chamar soma() com qualquer quantidade de números, incluindo nenhum. A tupla args será vazia nesse último caso.

3. **kwargs: argumentos nomeados variáveis

Enquanto *args lida com argumentos posicionais, **kwargs captura argumentos nomeados (também chamados de keyword arguments). Dentro da função, kwargs é um dicionário onde as chaves são os nomes dos parâmetros e os valores são os argumentos fornecidos.

def configurar(**kwargs):
    for chave, valor in kwargs.items():
        print(f"{chave} = {valor}")

configurar(host="localhost", porta=8080, debug=True)
# Saída:
# host = localhost
# porta = 8080
# debug = True

Este padrão é extremamente útil para funções que aceitam um conjunto flexível de opções de configuração.

4. Combinando args e *kwargs com parâmetros fixos

É possível combinar parâmetros fixos, *args e **kwargs em uma mesma função, desde que respeitada a ordem obrigatória:

  1. Parâmetros posicionais comuns
  2. *args (argumentos posicionais variáveis)
  3. Parâmetros nomeados (com valor padrão)
  4. **kwargs (argumentos nomeados variáveis)
def func(a, b, *args, opcao=True, **kwargs):
    print(f"a = {a}, b = {b}")
    print(f"args = {args}")
    print(f"opcao = {opcao}")
    print(f"kwargs = {kwargs}")

func(1, 2, 3, 4, 5, opcao=False, verbose=True, modo="teste")
# Saída:
# a = 1, b = 2
# args = (3, 4, 5)
# opcao = False
# kwargs = {'verbose': True, 'modo': 'teste'}

Um caso especial interessante é o uso do * sozinho, que força todos os parâmetros seguintes a serem nomeados:

def conectar(host, port, *, timeout=30, ssl=True):
    print(f"Conectando a {host}:{port} (timeout={timeout}, ssl={ssl})")

conectar("localhost", 8080, timeout=60)  # Válido
# conectar("localhost", 8080, 60)        # Erro! timeout deve ser nomeado

5. Desempacotamento de argumentos com * e **

Os operadores * e ** também podem ser usados no momento da chamada da função para desempacotar sequências e dicionários, respectivamente. Este é o mecanismo inverso do empacotamento feito por *args e **kwargs.

def apresentar(nome, idade, cidade):
    print(f"{nome} tem {idade} anos e mora em {cidade}")

# Desempacotamento de lista/tupla
dados_pessoa = ["Ana", 28, "São Paulo"]
apresentar(*dados_pessoa)  # Equivalente a apresentar("Ana", 28, "São Paulo")

# Desempacotamento de dicionário
dados_dict = {"nome": "Carlos", "idade": 35, "cidade": "Rio de Janeiro"}
apresentar(**dados_dict)   # Equivalente a apresentar(nome="Carlos", idade=35, cidade="Rio de Janeiro")

Esta técnica é frequentemente usada para combinar múltiplas fontes de dados ou para delegar chamadas entre funções.

6. Boas práticas e casos de uso comuns

Quando usar *args:
- Funções matemáticas que operam sobre coleções (soma, média, produto)
- Decoradores que precisam envolver funções com assinaturas desconhecidas
- Funções de logging que aceitam qualquer número de valores

def log(mensagem, *valores):
    print(f"[LOG] {mensagem}: {', '.join(str(v) for v in valores)}")

log("Valores processados", 10, 20, 30)

Quando usar **kwargs:
- Configurações de bibliotecas e frameworks
- Wrappers que repassam argumentos para funções internas
- Herança e chamadas a super() em classes

class Base:
    def __init__(self, **kwargs):
        self.config = kwargs

class Derivada(Base):
    def __init__(self, nome, **kwargs):
        super().__init__(**kwargs)
        self.nome = nome

Nomenclatura: Embora args e kwargs sejam apenas convenções (poderíamos usar *valores ou **opcoes), segui-las torna o código mais legível para outros programadores Python.

Cuidados: O uso excessivo de **kwargs pode dificultar a manutenção, pois esconde quais parâmetros são realmente esperados. Documente adequadamente ou considere usar dicionários tipados com TypedDict em projetos maiores.

7. Erros comuns e como evitá-los

Esquecer de desempacotar ao repassar argumentos:

def externa(*args):
    # ERRADO: passa a tupla inteira como um único argumento
    # interna(args)

    # CORRETO: desempacota a tupla
    interna(*args)

def interna(a, b, c):
    return a + b + c

Conflito entre *args e parâmetros nomeados padrão:

def problema(*args, opcao="padrao"):
    pass

# Chamada ambígua: 3 é argumento posicional ou nomeado?
# problema(3, opcao="teste")  # Funciona: 3 vai para args

# Para evitar confusão, use o operador * sozinho:
def solucao(*, opcao="padrao"):
    pass

Diferença entre vazio: Uma função pode ser chamada sem argumentos posicionais (*args vazio) ou sem argumentos nomeados (**kwargs vazio). Ambos são casos válidos e devem ser tratados adequadamente.

8. Exemplo integrado: aplicação prática

Vamos construir um decorador genérico que mede o tempo de execução de qualquer função, usando *args e **kwargs para aceitar qualquer assinatura:

import time
from functools import wraps

def temporizador(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        inicio = time.time()
        resultado = func(*args, **kwargs)
        fim = time.time()
        print(f"{func.__name__} executou em {fim - inicio:.4f} segundos")
        return resultado
    return wrapper

@temporizador
def calcular_media(*notas):
    return sum(notas) / len(notas) if notas else 0

@temporizador
def saudacao(nome, **opcoes):
    saudacao_base = f"Olá, {nome}!"
    if opcoes.get("formal"):
        return saudacao_base.upper()
    return saudacao_base

print(calcular_media(7, 8, 9, 10))  # Média: 8.5 + tempo de execução
print(saudacao("Maria", formal=True))  # OLÁ, MARIA! + tempo de execução

Este exemplo demonstra como *args e **kwargs são ferramentas essenciais no Python para criar código flexível, reutilizável e elegante. Eles permitem que funções e decoradores se adaptem a diferentes contextos sem sacrificar a clareza ou a funcionalidade.

Referências