El "menor asombro" y el argumento mutable del default

Resuelto Stefano Borini asked hace 15 años • 34 respuestas

Cualquiera que haya jugado con Python el tiempo suficiente ha sido mordido (o despedazado) por el siguiente problema:

def foo(a=[]):
    a.append(5)
    return a

Los principiantes en Python esperarían que esta función llamada sin parámetro siempre devuelva una lista con un solo elemento: [5]. En cambio, el resultado es muy diferente y muy sorprendente (para un novato):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Un gerente mío tuvo una vez su primer encuentro con esta característica y la llamó "un defecto de diseño dramático" del lenguaje. Respondí que el comportamiento tenía una explicación subyacente y que, de hecho, es muy desconcertante e inesperado si no se comprenden sus aspectos internos. Sin embargo, no pude responder (a mí mismo) la siguiente pregunta: ¿cuál es el motivo para vincular el argumento predeterminado en la definición de la función y no en la ejecución de la función? Dudo que el comportamiento experimentado tenga un uso práctico (¿quién realmente usó variables estáticas en C, sin generar errores?)

Editar :

Baczek dio un ejemplo interesante . Junto con la mayoría de sus comentarios y los de Utaal en particular , expuse más detalles:

def a():
    print("a executed")
    return []

           
def b(x=a()):
    x.append(5)
    print(x)

a executed
>>> b()
[5]
>>> b()
[5, 5]

Para mí, parece que la decisión de diseño fue relativa a dónde colocar el alcance de los parámetros: ¿dentro de la función o "junto" con ella?

Hacer el enlace dentro de la función significaría que xestá efectivamente vinculado al valor predeterminado especificado cuando se llama a la función, no definida, algo que presentaría un defecto profundo: la deflínea sería "híbrida" en el sentido de que parte del enlace (de el objeto de función) sucedería en la definición, y parte (asignación de parámetros predeterminados) en el momento de la invocación de la función.

El comportamiento real es más consistente: todo lo de esa línea se evalúa cuando se ejecuta esa línea, es decir, en la definición de la función.

Stefano Borini avatar Jul 16 '09 01:07 Stefano Borini
Aceptado

En realidad, esto no es un defecto de diseño y no se debe a aspectos internos o de rendimiento. Proviene simplemente del hecho de que las funciones en Python son objetos de primera clase, y no solo un fragmento de código.

Tan pronto como lo piensas de esta manera, entonces tiene mucho sentido: una función es un objeto que se evalúa según su definición; Los parámetros predeterminados son una especie de "datos de miembros" y, por lo tanto, su estado puede cambiar de una llamada a otra, exactamente como en cualquier otro objeto.

En cualquier caso, el effbot (Fredrik Lundh) tiene una muy buena explicación de los motivos de este comportamiento en Valores de parámetros predeterminados en Python . Lo encontré muy claro y realmente sugiero leerlo para conocer mejor cómo funcionan los objetos funcionales.

rob avatar Jul 17 '2009 21:07 rob

Supongamos que tiene el siguiente código

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Cuando veo la declaración de eat, lo menos sorprendente es pensar que si no se da el primer parámetro, será igual a la tupla("apples", "bananas", "loganberries")

Sin embargo, supongamos que más adelante en el código hago algo como

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

entonces, si los parámetros predeterminados estuvieran vinculados en la ejecución de la función en lugar de en la declaración de la función, me sorprendería (en el peor sentido) descubrir que se habían cambiado las frutas. En mi opinión, esto sería más sorprendente que descubrir que su foofunción anterior estaba mutando la lista.

El verdadero problema radica en las variables mutables, y todos los lenguajes tienen este problema hasta cierto punto. Aquí hay una pregunta: supongamos que en Java tengo el siguiente código:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Ahora bien, ¿mi mapa utiliza el valor de la StringBufferclave cuando se colocó en el mapa o almacena la clave por referencia? De cualquier manera, alguien queda asombrado; ya sea la persona que intentó sacar el objeto usando Mapun valor idéntico al que lo puso, o la persona que parece no poder recuperar su objeto a pesar de que la clave que está usando es literalmente el mismo objeto que se usó para ponerlo en el mapa (en realidad, esta es la razón por la que Python no permite que sus tipos de datos incorporados mutables se usen como claves de diccionario).

Su ejemplo es un buen caso en el que los recién llegados a Python se sorprenderán y morderán. Pero yo diría que si "arregláramos" esto, eso sólo crearía una situación diferente en la que serían mordidos, y esa sería aún menos intuitiva. Además, este es siempre el caso cuando se trata de variables mutables; Siempre te encuentras con casos en los que alguien podría esperar intuitivamente uno u otro comportamiento dependiendo del código que esté escribiendo.

Personalmente, me gusta el enfoque actual de Python: los argumentos de la función predeterminada se evalúan cuando se define la función y ese objeto siempre es el predeterminado. Supongo que podrían utilizar un caso especial usando una lista vacía, pero ese tipo de caso especial causaría aún más asombro, sin mencionar que sería incompatible con versiones anteriores.

Eli Courtwright avatar Jul 15 '2009 18:07 Eli Courtwright