Bugs de Argumentos Padrão Mutáveis
Valores de argumento padrão são avaliados uma vez no momento da definição da função. Padrões mutáveis (list, dict, set) tornam-se estado compartilhado entre chamadas, produzindo contaminação entre requisições e falhas "às vezes erradas" em testes.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
# BUG
def append_item(item, bucket=[]):
bucket.append(item)
return bucket
# CORREÇÃO
def append_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucketQuando usar isso:
- Manipuladores de API acumulam itens entre requisições não relacionadas
- Testes unitários passam sozinhos, mas falham quando executados como um conjunto
- Coletores de log ou métricas crescem indefinidamente entre chamadas
- Campos
@dataclassusam padrões mutáveis incorretamente (padrão relacionado)
Exemplo de Trabalho
# broken_session_store.py
from dataclasses import dataclass, field
def broken_add_token(user_id: str, token: str, store: dict[str, list[str]] = {}) -> dict[str, list[str]]:
store.setdefault(user_id, []).append(token)
return store
# sintomas
broken_add_token("u1", "a")
broken_add_token("u2", "b")
print(broken_add_token("u1", "c")) # u1 agora tem tokens da loja anterior poluída
# fixed_session_store.py
def add_token(
user_id: str,
token: str,
store: dict[str, list[str]] | None = None,
) -> dict[str, list[str]]:
if store is None:
store = {}
store.setdefault(user_id, []).append(token)
return store
@dataclass
class SessionStore:
_data: dict[str, list[str]] = field(default_factory=dict)
def add(self, user_id: str, token: str) -> None:
self._data.setdefault(user_id, []).append(token)
# tests/test_session_store.py
from fixed_session_store import SessionStore, add_token
def test_add_token_isolated_calls() -> None:
assert add_token("u1", "x") == {"u1": ["x"]}
assert add_token("u2", "y") == {"u2": ["y"]}
def test_session_store_dataclass() -> None:
a = SessionStore()
b = SessionStore()
a.add("u1", "t1")
assert b._data == {}O que isso demonstra:
- O
{}padrão é um dicionário compartilhado para todas as chamadas debroken_add_token - O sentinela
Nonecria um novo dicionário por chamada quando omitido @dataclassusafield(default_factory=dict)pela mesma razão- Testes provam isolamento entre instâncias e chamadas
Análise Detalhada
Como Funciona
- Padrões de função são vinculados na execução de
def, não por chamada - O mesmo objeto de lista/dicionário é reutilizado quando o chamador omite o argumento
- Padrões de campo de dataclass seguem a mesma regra sem
default_factory - Padrões imutáveis (
None,0,"", tuplas) são seguros
Padrões Seguros vs. Inseguros
| Padrão | Seguro? | Notas |
|---|---|---|
None | sim | use sentinela + mutável novo dentro |
Tupla () | sim | imutável |
[], {}, set() | não | compartilhado entre chamadas |
datetime.now() | não | congelado no momento da definição |
Notas de Python
# a tipagem torna a intenção clara
def process(items: list[int] | None = None) -> list[int]:
working = list(items) if items is not None else []
working.append(1)
return workingArmadilhas
- Apenas algumas requisições afetadas - bug heisenbug sob concorrência reutiliza a memória do processo de trabalho. Correção: remova padrões mutáveis; reiniciar mascara, mas não corrige.
- Campo de dataclass
= {}- mesmo bug em classes. Correção:field(default_factory=dict). - Copiar o padrão manualmente incorretamente -
bucket = bucket or []trata lista vazia como ausente. Correção:if bucket is None. - Mutação da lista do chamador quando fornecida - efeito colateral surpreendente quando o padrão não é usado. Correção:
working = list(bucket)se você não deve criar um alias para a entrada. - Cache com
@lru_cacheem métodos com estado mutável oculto - cache + mutação agravam a confusão. Correção: funções puras ou estado de instância explícito. - FastAPI
Dependscom padrão mutável - raro, mas possível em assinaturas de dependência personalizadas. Correção: use o padrão sentinelaNone.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
Sentinela None | funções gerais | você precisa de um "vazio" distinguível de terceiro estado |
dataclasses.field(default_factory=...) | classes | funções simples |
| Nova instância de classe por requisição | estado de escopo de requisição FastAPI | simples helpers puros |
FAQs
Por que Python avalia padrões no momento da definição?
Padrões são armazenados no objeto da função (__defaults__, __kwdefaults__) para desempenho e introspecção. Objetos mutáveis não devem ser usados como esses valores armazenados.
É `def f(x=time.time())` seguro?
Não. O timestamp é fixado na importação/definição. Passe None e compute time.time() dentro do corpo da função.
Como encontro esses bugs em uma base de código grande?
Procure por = [], = {}, = set() em linhas de def e campos de dataclass sem default_factory. ruff 0.9+ regras sinalizam alguns padrões.
O asyncio muda isso?
O mesmo objeto de função é compartilhado entre tarefas em um loop de eventos. Padrões mutáveis vazam estado entre requisições concorrentes em um processo.
E sobre `list` padrão em modelos Pydantic?
Pydantic 2 usa default_factory internamente quando você usa Field(default_factory=list). items: list = [] simples em dataclasses crus ainda é inseguro.
Posso usar `bucket=[]` se eu nunca mutar?
Se você realmente nunca mutar, um padrão de tupla é mais claro. Padrões de lista vazia ainda confundem leitores e linters.
Por que não `bucket = bucket or []`?
Uma lista vazia [] é falsy; chamadores passando [] receberiam uma nova lista inesperadamente. is None verifica apenas o sentinela.
Globais em nível de módulo são melhores?
Não. Globais criam os mesmos problemas de estado compartilhado com testabilidade pior. Prefira parâmetros explícitos ou atributos de instância.
Como depurar um padrão compartilhado suspeito?
Imprima id(store) entre chamadas sem passar store. IDs idênticos confirmam o objeto de padrão compartilhado.
Flask `g` ou estado de requisição FastAPI substituem este padrão?
O estado de escopo de requisição pertence a objetos g/requisição, não a padrões de função. Padrões são para todo o processo, não para toda a requisição.
E sobre `functools.partial` com listas?
Partial vincula argumentos cedo; certifique-se de que objetos mutáveis vinculados não sejam compartilhados não intencionalmente entre partials.
Deepcopy no padrão é uma correção?
copy.deepcopy por chamada funciona, mas é mais lento e obscuro. O sentinela None é idiomático e mais rápido.
Relacionado
- Modelagem de Domínio -
default_factoryde dataclass - Cenários de Vazamento de Memória - crescimento ilimitado de caches compartilhados
- Snippets de Refatoração de Depuração - antes/depois das correções
- Ferramentas de Depuração - inspecionar com pdb
- Melhores Práticas de Arquitetura - evitar estado mutável compartilhado
Versões da Pilha: Esta página foi escrita para Python 3.14.0 (estável 3.14, manutenção 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, e uv 0.6+.