Blog

Argumentos por defecto mutables en Python

Un argumento por defecto en Python se evalúa una sola vez, cuando se define la función. Si ese valor por defecto es una lista o un diccionario, todas las llamadas comparten el mismo objeto.

Esta es la sorpresa más reportada de Python, y produce errores que parecen decir que la función recuerda llamadas anteriores.

El error

Tres llamadas, cada una pensada para empezar con una canasta vacía.

# the bug
def add_item(item, basket=[]):
    basket.append(item)
    return basket

print(add_item('apple'))
print(add_item('bread'))
print(add_item('milk'))

Imprime:

['apple']
['apple', 'bread']
['apple', 'bread', 'milk']

La canasta sigue creciendo. Nadie se la pasó, y nunca se vació.

Por qué pasa

El valor por defecto se guarda en el objeto función mismo. Puedes mirarlo directamente.

# why it happens
def add_item2(item, basket=[]):
    basket.append(item)
    return basket

print(add_item2.__defaults__)
add_item2('x')
print(add_item2.__defaults__)

Imprime:

([],)
(['x'],)

__defaults__ guarda la misma lista que la función reparte. Agregar dentro del cuerpo agrega a esa lista guardada, así que la siguiente llamada la ve.

La solución

Usa None como valor por defecto y construye el valor real dentro de la función.

# 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'))

Imprime:

['apple']
['bread']

Ahora la lista se crea en cada llamada, porque el cuerpo corre en cada llamada.

Aplica a todo valor por defecto mutable

Las listas, los diccionarios y los conjuntos se comportan así. Los valores por defecto inmutables como números, cadenas, tuplas y fechas son seguros, porque nada los puede cambiar.

# 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'))

Imprime:

(1, 1)
(2, 2)

El valor por defecto de la fecha está bien. El diccionario y la lista no.

La misma trampa en el cuerpo de una clase

Una lista asignada en el cuerpo de la clase pertenece a la clase, así que todas las instancias la comparten.

# 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)

Imprime:

['apple'] True
[] False

Asigna los atributos mutables en __init__, no en el cuerpo de la clase.

Qué recordar

  • Los argumentos por defecto se evalúan una sola vez, al definir la función.

  • Un valor por defecto mutable lo comparten todas las llamadas que no pasen uno.

  • Usa None como valor por defecto y crea el valor dentro de la función.

  • La misma regla aplica a los atributos mutables en el cuerpo de una clase.

Las dataclasses lanzan un ValueError si intentas esto, que es uno de los pocos lugares donde Python te detiene en vez de dejar que pase en silencio.

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.