El "problema"

Es posible que al usar funciones con parámetros por defecto se encuentren con cierto comportamiento inesperado o poco intuitivo de Python. Por estas cosas siempre hay que revisar el código, conocerlo lo mejor posible y saber responder cuando las cosas no funcionan como uno espera.

Veamos el comportamiento de los parametros por defecto en funciones


In [1]:
def funcion(lista=[]):
    lista.append(1)
    print("La lista vale: {}".format(lista))

Si llamamos a la función una vez...


In [2]:
funcion()


La lista vale: [1]

... todo funciona como lo suponemos, pero y si probamos otra vez...


In [3]:
funcion()
funcion()


La lista vale: [1, 1]
La lista vale: [1, 1, 1]

... ok? No funciona como lo supondriamos.

Esto también podemos extenderlo a clases, donde es comun usar parámetros por defecto:


In [4]:
class Clase:

    def __init__(self, lista=[]):
        self.lista = lista
        self.lista.append(1)
        print("Lista de la clase: {}".format(self.lista))

# Instanciamos dos objetos
A = Clase()
B = Clase()

# Modificamos el parametro en una
A.lista.append(5)

# What??
print(A.lista)
print(B.lista)


Lista de la clase: [1]
Lista de la clase: [1, 1]
[1, 1, 5]
[1, 1, 5]

Investigando nuestro código

Veamos un poco qué está pasando en nuestro código:


In [5]:
# Instanciemos algunos objetos
A = Clase()
B = Clase()
C = Clase(lista=["GG"]) # Usaremos esta isntancia como control

print("\nLos objetos son distintos!")
print("id(A): {} \nid(B): {} \nid(C): {}".format(id(A), id(B), id(C)))

print("\nPero la lista es la misma para A y para B :O")
print("id(A.lista): {} \nid(B.lista): {} \nid(C.lista): {}".format(id(A.lista), id(B.lista), id(C.lista)))


Lista de la clase: [1, 1, 5, 1]
Lista de la clase: [1, 1, 5, 1, 1]
Lista de la clase: ['GG', 1]

Los objetos son distintos!
id(A): 72497248 
id(B): 72497192 
id(C): 72499096

Pero la lista es la misma para A y para B :O
id(A.lista): 72545608 
id(B.lista): 72545608 
id(C.lista): 68790472

¿Qué está pasando? D:

En Python, las funciones son objetos del tipo callable, es decir, que son llamables, ejecutan una operación.


In [6]:
# De hecho, tienen atributos...

def funcion(lista=[]):
    lista.append(5)
    
# En la funcion "funcion"...
print("{}".format(funcion.__defaults__))

# ... si la invocamos...
funcion()

# ahora tenemos...
print("{}".format(funcion.__defaults__))


# Si vemos como quedo el metodo "__init__" de la clase Clase...
print("{}".format(Clase.__init__.__defaults__))


([],)
([5],)
([1, 1, 5, 1, 1],)

El código que define a función es evaluado una vez y dicho valor evaluado es el que se usa en cada llamado posterior. Por lo tanto, al modificar el valor de un parámetro por defecto que es mutable (list, dict, etc.) se modifica el valor por defecto para el siguiente llamado.

¿Cómo evitar esto?

Una solución simple es usar None como el valor predeterminado para los parámetros por defecto. Y otra solución es la declaración de variables condicionales:


In [7]:
class Clase:
    
    def __init__(self, lista=None):
        # Version "one-liner":
        self.lista = lista if lista is not None else list()
        
        # En su version extendida:
        if lista is not None:
            self.lista = lista
        else:
            self.lista = list()

Importante: Esto no es un bug/error/magia negra... Es Python. En Python todo es un objeto, incluso las funciones...

Recursos sobre el tema:

  • StackOverflow - “Least Astonishment” in Python: The Mutable Default Argument [link]
  • Effbot.org - Default Parameter Values in Python [link]
  • Python Docs - Compound statements > Function definitions [link]
  • Python Docs - Data model > The standard type hierarchy [link]