Um argumento padrão em Python é avaliado uma vez, quando a função é definida. Se esse padrão for uma lista ou um dicionário, todas as chamadas compartilham o mesmo objeto.
Essa é a surpresa mais relatada em Python, e ela produz bugs que parecem uma função lembrando das chamadas anteriores.
O bug
Três chamadas, cada uma supostamente começando com uma cesta vazia.
# the bug
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item('apple'))
print(add_item('bread'))
print(add_item('milk'))
Ele imprime:
['apple']
['apple', 'bread']
['apple', 'bread', 'milk']
A cesta não para de crescer. Ninguém passou ela, e ela nunca foi esvaziada.
Por que isso acontece
O padrão fica guardado no próprio objeto função. Você pode olhar direto para ele.
# why it happens
def add_item2(item, basket=[]):
basket.append(item)
return basket
print(add_item2.__defaults__)
add_item2('x')
print(add_item2.__defaults__)
Ele imprime:
([],)
(['x'],)
__defaults__ guarda a mesma lista que a função entrega. Dar append dentro do corpo dá append nessa lista guardada, então a próxima chamada enxerga o resultado.
A correção
Use None como padrão e construa o valor real dentro da função.
# the fix
def add_item3(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item3('apple'))
print(add_item3('bread'))
Ele imprime:
['apple']
['bread']
Agora a lista é criada a cada chamada, porque o corpo roda a cada chamada.
Vale para todo padrão mutável
Listas, dicionários e sets se comportam assim. Padrões imutáveis como números, strings, tuplas e datas são seguros, porque nada pode alterá-los.
# it applies to every mutable default
import datetime
def log(message, seen={}, stamps=[], when=datetime.date(2020, 1, 1)):
seen[message] = True
stamps.append(when)
return len(seen), len(stamps)
print(log('first'))
print(log('second'))
Ele imprime:
(1, 1)
(2, 2)
O padrão da data está tudo bem. O dicionário e a lista não estão.
A mesma armadilha no corpo da classe
Uma lista atribuída no corpo da classe pertence à classe, então todas as instâncias compartilham ela.
# the same trap in a class body
class Basket:
items = [] # shared by every instance
a, b = Basket(), Basket()
a.items.append('apple')
print(b.items, a.items is b.items)
class BasketFixed:
def __init__(self):
self.items = []
c, d = BasketFixed(), BasketFixed()
c.items.append('apple')
print(d.items, c.items is d.items)
Ele imprime:
['apple'] True
[] False
Atribua atributos mutáveis no __init__, não no corpo da classe.
O que lembrar
-
Argumentos padrão são avaliados uma vez, no momento da definição.
-
Um padrão mutável é compartilhado por toda chamada que não passa um valor.
-
Use
Nonecomo padrão e crie o valor dentro da função. -
A mesma regra vale para atributos mutáveis no corpo de uma classe.
Dataclasses levantam um ValueError se você tentar isso, que é um dos poucos lugares em que o Python te impede em vez de deixar acontecer em silêncio.