2009-10-30 14 views
32

Tuve un momento muy difícil para 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.¿Por qué los argumentos predeterminados se evalúan en tiempo de definición en Python?

El código es el siguiente:

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

El problema es que cada instancia de acciones clase Node la misma children de atributos, si el atributo no se da de forma explícita, tales 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 deben evaluarse en el momento de la definición? Esto me parece muy contrario a la intuición.

+0

Mi conjetura sería el rendimiento. Imagínese reevaluar cada vez que se llame a una función si se llama 15 millones de veces al día. –

Respuesta

38

La alternativa sería bastante pesada: almacenar "valores de argumento predeterminados" en el objeto de función como "thunks" de código para ejecutar una y otra vez cada vez que se invoca la función sin un valor especificado para ese argumento - y haría mucho más difícil obtener un enlace anticipado (vinculante en el momento de la definición), que a menudo es lo que desea. Por ejemplo, en Python, ya que 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] 

... escribiendo una función memoized como el de arriba es una tarea muy elemental.Del mismo modo:

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

... la sencilla i=i, basándose en el (tiempo de definición) de enlace temprano de los valores por defecto arg, es una forma trivial simple para obtener el enlace anticipado. Entonces, 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 vincular tarde el valor de una expresión, evalúe esa expresión en el cuerpo de la función; si desea una vinculación anticipada, evalúela como el valor predeterminado de una arg.

La alternativa, forzando el encuadernado tardío para ambas situaciones, no ofrecería esta flexibilidad, y le obligaría a pasar por aros (como envolver su función en una fábrica de cierre) cada vez que necesitara un encuadernado temprano, como en el ejemplo anterior ejemplos: una regla hipotética de diseño (más allá de los "invisibles" de generación y evaluación repetida de thunks por todas partes) forzó al programador a repetir más peso pesado.

En otras palabras, "Debería haber una, y preferiblemente solo una, forma obvia de hacerlo [1]": cuando desea la vinculación tardía, ya existe una forma perfectamente obvia de lograrlo (ya que todas las funciones el código solo se ejecuta en el momento de la llamada, obviamente, todo lo que se evalúe allí está retrasado); tener una evaluación por defecto de arg produce una vinculación anticipada que le proporciona una forma obvia de lograr un enlace anticipado también (¡más! -) en lugar de dar DOS formas obvias de obtener un enlace tardío y ninguna forma obvia de obtener un enlace anticipado (¡un menos! -).

[1]: "Aunque tal vez no sea obvio al principio a menos que sea holandés".

+0

excelente respuesta, +1 de mí. Un error muy leve: debe ser return _memo [key] con un guion bajo inicial. – Francesco

+0

@Francesco, tx para señalar el error tipográfico (y me imagino tx @ novelocrat por arreglarlo tan pronto! -). –

+0

¿Sería la sobrecarga prohibitiva en caso de una copia profunda en lugar de una evaluación demorada? –

0

Las definiciones de las funciones de Python son solo código, como todo el otro código; no son "mágicos" en la forma en que lo son algunos idiomas. Por ejemplo, en Java que podría referirse "ahora" a algo definido "después":

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 

Por lo tanto, el argumento por defecto se evalúa en el momento en que esa línea de código es evaluado!

+1

Mala analogía. El equivalente pitónico de tu muestra java está insertando 'if __name__ == '__main__': main()' al final del archivo – hasen

7

La solución para esto, discussed here (y muy sólido), es decir:

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

cuanto a por qué buscar una respuesta de von Löwis, pero es probable debido a la definición de función hace que 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 los argumentos predeterminados.

+1

Hola Jed, puede haber algún problema (raro) cuando las entradas que no sean [] pueden ocurrir que evalúen a Falso. Entonces, una entrada legítima puede transformarse en []. Por supuesto, esto no puede suceder, siempre y cuando los niños deben ser una lista. – Juergen

+0

... de olvidado: Más general sería "si los niños son Ninguno ..." – Juergen

+0

El "si los niños es Ninguno: niños = []" (seguido de "self.children = children" aquí) es equivalente (casi- --degenerar los valores sería diferente) y mucho más legible. –

7

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

También usted debe saber, que en el caso de los tipos de contenedores se puede producir este problema - pero usted podría evitarlo haciendo lo explícito:

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

también puede acortarlo a 'self.children = children o []' en lugar de tener la instrucción if. –

+0

¿Qué sucede si lo llamo con (niños = Ninguno). A continuación, creará de forma incorrecta children = []. Para arreglar esto, necesitaría usar un valor centinela. –

+0

En este caso, asumí silenciosamente que Ninguno es un valor centinela apropiado. Por supuesto, si None podría ser un valor válido (en el caso de niños (muy probablemente una lista de cosas) improbable), se debe usar un valor de centinela diferente. Si no existe un valor estándar, use un objeto especialmente creado para esto. – Juergen

4

Esto viene de énfasis de pitón en la sintaxis y la sencillez de ejecución. una sentencia def ocurre en un 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 la declaración de la función y el cuerpo de la función. La declaración se ejecuta cuando se alcanza en el código. El cuerpo se ejecuta en el tiempo de llamada. Tenga en cuenta que la declaración se ejecuta cada vez que se alcanza, por lo que puede crear múltiples funciones mediante 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 creada cada vez que se ejecutaba la declaración de la 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 le da a 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 te han dado la solución, de la utilización de parámetro = Ninguno, y la asignación de una lista en el cuerpo si el valor es Ninguno, lo que es totalmente pitón idiomática. Es un poco feo, pero la simplicidad es poderosa, y la solución no es demasiado dolorosa.

Editado para añadir: Para más discusión sobre esto, ver 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

10

La cuestión es la siguiente.

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

  • 0 es un simple literal. Evaluarlo una vez, usarlo para siempre.

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

El constructo [] es literal, como 0, que significa "este objeto exacta".

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 necesaria if 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 para implementar los valores por defecto de los 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, les permitimos que sean funciones que se evalúan cada vez que se requieren los valores predeterminados de un parámetro.

[Este sería el camino paralelo collections.defaultdict obras.]

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. Oops.

+0

+1 por la circularidad horror – hasen

+3

Obtengo la parte "sería costoso", pero la parte "es imposible" no la obtengo. No puede ser imposible cuando hay otros lenguajes dinámicos interpretados que lo hacen –

5

Pensé que esto también era contrario a la intuición, 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 declaración def, los coloca en una tupla y agrega esa tupla como un atributo de la función denominada 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>,) 

De manera que todas las llamadas a f que no proporcionan un argumento va a utilizar 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 obvio inmediato de rendimiento, comienza a entrar en un universo de casos especiales, como almacenar valores literales en lugar de funciones para tipos no modificables para evitar llamadas a funciones innecesarias. Y, por supuesto, hay implicaciones de rendimiento en abundancia.

El comportamiento real es realmente simple. Y hay una solución trivial, en el caso en el que desea un valor por defecto a ser producido por una llamada de función en tiempo de ejecución:

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

Porque si lo hubieran hecho, entonces alguien podría enviar una pregunta preguntando por qué no era' t al revés :-p

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

Cuestiones relacionadas