Blog

Argumentos padrão mutáveis em Python

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 None como 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.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.