¿Por qué se evalúan los argumentos predeterminados en tiempo de definición en Python?

StackOverflow https://stackoverflow.com/questions/1651154

  •  22-07-2019
  •  | 
  •  

Pregunta

Me costó mucho entender la causa raíz de un problema en un algoritmo. Luego, al simplificar las funciones paso a paso, descubrí que la evaluación de los argumentos predeterminados en Python no se comporta como esperaba.

El código es el siguiente:

class Node(object):
    def __init__(self, children = []):
        self.children = children

El problema es que cada instancia de la clase Node comparte el mismo atributo children , si el atributo no se proporciona explícitamente, como:

>>> n0 = Node()
>>> n1 = Node()
>>> id(n1.children)
Out[0]: 25000176
>>> id(n0.children)
Out[0]: 25000176

¿No entiendo la lógica de esta decisión de diseño? ¿Por qué los diseñadores de Python decidieron que los argumentos predeterminados se evaluarán en el momento de la definición? Esto me parece muy intuitivo.

¿Fue útil?

Solución

La alternativa sería bastante pesada: almacenar " valores de argumento predeterminados " en el objeto de función como '' thunks '' de código que se ejecutará una y otra vez cada vez que se llame a la función sin un valor específico para ese argumento, y haría mucho más difícil obtener un enlace temprano (enlace en el momento de la definición), que a menudo es lo que desea. Por ejemplo, en Python tal como existe:

def ack(m, n, _memo={}):
  key = m, n
  if key not in _memo:
    if m==0: v = n + 1
    elif n==0: v = ack(m-1, 1)
    else: v = ack(m-1, ack(m, n-1))
    _memo[key] = v
  return _memo[key]

... escribir una función memorable como la anterior es una tarea bastante elemental. Del mismo modo:

for i in range(len(buttons)):
  buttons[i].onclick(lambda i=i: say('button %s', i))

... el simple i = i , que se basa en el enlace temprano (tiempo de definición) de los valores arg predeterminados, es una forma trivialmente simple de obtener un enlace temprano. Por lo tanto, la regla actual es simple, directa y le permite hacer todo lo que quiera de una manera extremadamente fácil de explicar y comprender: si desea un enlace tardío del valor de una expresión, evalúe esa expresión en el cuerpo de la función; si desea un enlace temprano, evalúelo como el valor predeterminado de un argumento.

La alternativa, forzar el enlace tardío para ambas situaciones, no ofrecería esta flexibilidad y lo obligaría a pasar por aros (como envolver su función en una fábrica de cierre) cada vez que necesita un enlace temprano, como en los ejemplos anteriores - aún más repetitivo de peso pesado forzado en el programador por esta decisión de diseño hipotética (más allá de los "invisibles" de generar y evaluar repetidamente thunks por todo el lugar).

En otras palabras, " Debe haber una, y preferiblemente solo una, forma obvia de hacerlo [1] ": cuando desea un enlace tardío, ya existe una forma perfectamente obvia de lograrlo (ya que todas el código de la función solo se ejecuta en el momento de la llamada, obviamente, todo lo evaluado allí está sujeto a retraso); el hecho de que la evaluación por defecto produzca un enlace temprano también le brinda una forma obvia de lograr el enlace temprano (¡un plus! -) en lugar de dar DOS formas obvias de obtener un enlace tardío y ninguna forma obvia de obtener un enlace temprano (un menos! -).

[1]: " Aunque esa forma puede no ser obvia al principio a menos que sea holandés. "

Otros consejos

El problema es este.

Es demasiado costoso evaluar una función como inicializador cada vez que se llama a la función .

  • 0 es un literal simple. Evalúelo una vez, úselo para siempre.

  • int es una función (como la lista) que debería evaluarse cada vez que se requiera como inicializador.

La construcción [] es literal, como 0 , que significa "este objeto exacto".

El problema es que algunas personas esperan que signifique list como en " evalúe esta función para mí, por favor, para obtener el objeto que es el inicializador " ;.

Sería una carga abrumadora agregar la declaración if necesaria para hacer esta evaluación todo el tiempo. Es mejor tomar todos los argumentos como literales y no hacer ninguna evaluación de función adicional como parte de intentar hacer una evaluación de función.

Además, más fundamentalmente, es técnicamente imposible implementar valores predeterminados de argumentos como evaluaciones de funciones.

Considere, por un momento, el horror recursivo de este tipo de circularidad. Digamos que en lugar de que los valores predeterminados sean literales, permitimos que sean funciones que se evalúan cada vez que se requieren los valores predeterminados de un parámetro.

[Esto sería paralelo a la forma en que funciona collections.defaultdict .]

def aFunc( a=another_func ):
    return a*2

def another_func( b=aFunc ):
    return b*3

¿Cuál es el valor de another_func () ? Para obtener el valor predeterminado para b , debe evaluar aFunc , que requiere una evaluación de another_func . ¡Vaya!

Por supuesto, en su situación es difícil de entender. Pero debe ver, que evaluar los argumentos predeterminados cada vez supondría una pesada carga de tiempo de ejecución para el sistema.

También debe saber que, en el caso de los tipos de contenedores, este problema puede ocurrir, pero puede evitarlo haciendo explícito el asunto:

def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children

La solución para esto, discutido aquí (y muy sólido), es:

class Node(object):
    def __init__(self, children = None):
        self.children = [] if children is None else children

En cuanto a por qué buscar una respuesta de von Löwis, pero es probable porque la definición de la función crea un objeto de código debido a la arquitectura de Python, y es posible que no haya una facilidad para trabajar con tipos de referencia como este en argumentos predeterminados.

Pensé que esto también era contradictorio, hasta que aprendí cómo Python implementa los argumentos predeterminados.

Una función es un objeto. En el momento de la carga, Python crea el objeto de función, evalúa los valores predeterminados en la instrucción def , los coloca en una tupla y agrega esa tupla como un atributo de la función llamada func_defaults . Luego, cuando se llama a una función, si la llamada no proporciona un valor, Python toma el valor predeterminado de func_defaults .

Por ejemplo:

>>> class C():
        pass

>>> def f(x=C()):
        pass

>>> f.func_defaults
(<__main__.C instance at 0x0298D4B8>,)

Por lo tanto, todas las llamadas a f que no proporcionan un argumento utilizarán la misma instancia de C , porque ese es el valor predeterminado.

En cuanto a por qué Python lo hace de esta manera: bueno, esa tupla podría contener funciones que se llamarían cada vez que se necesitara un valor de argumento predeterminado. Además del problema inmediatamente obvio del rendimiento, comienza a entrar en un universo de casos especiales, como almacenar valores literales en lugar de funciones para tipos no mutables para evitar llamadas innecesarias a funciones. Y, por supuesto, hay muchas implicaciones de rendimiento.

El comportamiento real es realmente simple. Y hay una solución trivial, en el caso de que quiera que se produzca un valor predeterminado mediante una llamada de función en tiempo de ejecución:

def f(x = None):
   if x == None:
      x = g()

Esto viene del énfasis de Python en la sintaxis y la simplicidad de ejecución. una declaración def ocurre en cierto punto durante la ejecución. Cuando el intérprete de Python llega a ese punto, evalúa el código en esa línea y luego crea un objeto de código a partir del cuerpo de la función, que se ejecutará más tarde, cuando llame a la función.

Es una división simple entre declaración de función y cuerpo de función. La declaración se ejecuta cuando se alcanza en el código. El cuerpo se ejecuta en el momento de la llamada. Tenga en cuenta que la declaración se ejecuta cada vez que se alcanza, por lo que puede crear múltiples funciones haciendo un bucle.

funcs = []
for x in xrange(5):
    def foo(x=x, lst=[]):
        lst.append(x)
        return lst
    funcs.append(foo)
for func in funcs:
    print "1: ", func()
    print "2: ", func()

Se han creado cinco funciones separadas, con una lista separada cada vez que se ejecuta la declaración de función. En cada ciclo a través de funcs , la misma función se ejecuta dos veces en cada paso, utilizando la misma lista cada vez. Esto da los resultados:

1:  [0]
2:  [0, 0]
1:  [1]
2:  [1, 1]
1:  [2]
2:  [2, 2]
1:  [3]
2:  [3, 3]
1:  [4]
2:  [4, 4]

Otros le han dado la solución, de usar param = None y asignar una lista en el cuerpo si el valor es None, que es una pitón completamente idiomática. Es un poco feo, pero la simplicidad es poderosa y la solución no es demasiado dolorosa.

Editado para agregar: Para más discusión sobre esto, vea el artículo de effbot aquí: http: // effbot.org/zone/default-values.htm , y la referencia del lenguaje, aquí: http://docs.python.org/reference/compound_stmts.html#function

Las definiciones de funciones de Python son solo código, como todos los demás códigos; no son "mágicos" en la forma en que algunos idiomas son. Por ejemplo, en Java podría referirse " ahora " a algo definido " más tarde " ;:

public static void foo() { bar(); }
public static void main(String[] args) { foo(); }
public static void bar() {}

pero en Python

def foo(): bar()
foo()   # boom! "bar" has no binding yet
def bar(): pass
foo()   # ok

¡Entonces, el argumento predeterminado se evalúa en el momento en que se evalúa esa línea de código!

Porque si lo hubieran hecho, alguien publicaría una pregunta preguntando por qué no fue al revés :-p

Supongamos ahora que lo hicieron. ¿Cómo implementaría el comportamiento actual si fuera necesario? Es fácil crear nuevos objetos dentro de una función, pero no puede "crear". ellos (puedes eliminarlos, pero no es lo mismo).

Proporcionaré una opinión disidente, agregando los argumentos principales en las otras publicaciones.

  

Evaluar los argumentos predeterminados cuando se ejecuta la función sería malo para el rendimiento.

Me resulta difícil de creer. Si las asignaciones de argumentos predeterminadas como foo = 'some_string' realmente agregan una cantidad inaceptable de sobrecarga, estoy seguro de que sería posible identificar asignaciones a literales inmutables y calcularlas previamente.

  

Si desea una asignación predeterminada con un objeto mutable como foo = [] , simplemente use foo = None , seguido de foo = foo o [] en el cuerpo de la función.

Si bien esto puede no ser problemático en casos individuales, como patrón de diseño no es muy elegante. Agrega código repetitivo y oculta los valores de argumento predeterminados. Patrones como foo = foo o ... no funcionan si foo puede ser un objeto como una matriz numpy con un valor de verdad indefinido. Y en situaciones donde None es un valor de argumento significativo que puede pasarse intencionalmente, no puede usarse como un centinela y esta solución se vuelve realmente fea.

  

El comportamiento actual es útil para objetos predeterminados mutables que deberían compartirse entre llamadas de función.

Me encantaría ver evidencia de lo contrario, pero en mi experiencia este caso de uso es mucho menos frecuente que los objetos mutables que deberían crearse de nuevo cada vez que se llama a la función. Para mí también parece un caso de uso más avanzado, mientras que las asignaciones predeterminadas accidentales con contenedores vacíos son un problema común para los nuevos programadores de Python. Por lo tanto, el principio de menor asombro sugiere que los valores de argumento predeterminados deben evaluarse cuando se ejecuta la función.

Además, me parece que existe una solución fácil para los objetos mutables que deberían compartirse entre las llamadas a funciones: inicialícelos fuera de la función.

Entonces diría que esta fue una mala decisión de diseño. Supongo que fue elegido porque su implementación es realmente más simple y porque tiene un caso de uso válido (aunque limitado). Desafortunadamente, no creo que esto cambie, ya que los desarrolladores principales de Python quieren evitar una repetición de la cantidad de incompatibilidad hacia atrás que introdujo Python 3.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top