propias - parametros por omision python
¿Por qué los argumentos predeterminados se evalúan en tiempo de definición en Python? (8)
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.
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 secundario, si el atributo no se proporciona explícitamente, como por ejemplo:
>>> 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.
El problema es esto.
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 exacto".
El problema es que algunas personas esperan que sea una list
medios como en "evaluar 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 los valores por defecto de los argumentos como evaluaciones de funciones.
Considera, 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.
[Esto sería similar 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
. Oops.
Esto proviene del énfasis de Python en la sintaxis y simplicidad 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 simple división 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 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 ejecuta la declaración de la función. En cada bucle a través de los funcs
, la misma función se ejecuta dos veces en cada paso, usando 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 totalmente idiomático. Es un poco feo, pero la simplicidad es poderosa, y la solución no es demasiado dolorosa.
Editado para agregar: para obtener más información al respecto, consulte el artículo de effbot aquí: http://effbot.org/zone/default-values.htm , y la referencia del idioma, aquí: http://docs.python.org/reference/compound_stmts.html#function
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án una y otra vez cada vez que se llame a la función sin un valor especificado para ese argumento, y lo haría es mucho más difícil conseguir un enlace temprano (vinculante en el momento de la definición), que a menudo es lo que quieres. 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 memorada como la anterior es una tarea bastante elemental. Similar:
for i in range(len(buttons)):
buttons[i].onclick(lambda i=i: say(''button %s'', i))
... el simple i=i
, confiando en el enlace temprano (tiempo de definición) de los valores de arg predeterminados, es una manera trivialmente simple de obtener un 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 entender: 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 la vinculación tardía para ambas situaciones, no ofrecería esta flexibilidad y te obligaría a pasar por aros (como envolver tu función en una fábrica de cierre) cada vez que necesites un enlace anticipado, como en los ejemplos anteriores, pero más peso pesado repetitivo forzado en el programador por esta decisión hipotética de diseño (más allá de los "invisibles" de generar y evaluar repetidamente thunks por todas partes).
En otras palabras, "debe haber una, y preferiblemente solo una, forma obvia de hacerlo [1]": cuando se desea vinculación tardía, ya existe una forma perfectamente obvia de lograrlo (ya que todo el código de la función solo se ejecuta en la hora de la llamada, obviamente, todo lo evaluado allí está atrasado); 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 seas holandés".
La solución para esto, discutida aquí (y muy sólida), 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, es probable porque la definición de la función hace 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.
Las definiciones de 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 puede referirse "ahora" a algo definido "más adelante":
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.
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 la 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 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 proporcionen un argumento usará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 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 de que desee que se produzca un valor predeterminado mediante una llamada a función en tiempo de ejecución:
def f(x = None):
if x == None:
x = g()
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 debe saber que, en el caso de los tipos de contenedores, este problema puede ocurrir, pero puede eludirlo explicándolo:
def __init__(self, children = None):
if children is None:
children = []
self.children = children
Porque si lo hubieran hecho, entonces alguien publicaría una pregunta preguntando por qué no era 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 "descrearlos" (puede eliminarlos, pero no es lo mismo).