Argumento predeterminado mutable de Python: ¿por qué?

20

Sé que los argumentos predeterminados se crean en el momento de inicialización de la función y no cada vez que se llama a la función. Ver el siguiente código:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

Lo que no entiendo es por qué esto se implementó de esta manera. ¿Qué beneficios ofrece este comportamiento sobre una inicialización en cada llamada?

Sardathrion - Restablece a Monica
fuente
Esto ya se discute en asombroso detalle en Stack Overflow: stackoverflow.com/q/1132941/5419599
Wildcard

Respuestas:

14

Creo que la razón es la simplicidad de implementación. Déjame elaborar.

El valor predeterminado de la función es una expresión que debe evaluar. En su caso, es una expresión simple que no depende del cierre, pero puede ser algo que contenga variables libres def ook(item, lst = something.defaultList()). Si va a diseñar Python, tendrá una opción: ¿lo evalúa una vez cuando se define la función o cada vez que se llama a la función? Python elige el primero (a diferencia de Ruby, que va con la segunda opción).

Hay algunos beneficios para esto.

Primero, obtienes un poco de velocidad y aumenta la memoria. En la mayoría de los casos, tendrá argumentos predeterminados inmutables y Python puede construirlos solo una vez, en lugar de en cada llamada a la función. Esto ahorra (algo) de memoria y tiempo. Por supuesto, no funciona bastante bien con valores mutables, pero usted sabe cómo puede moverse.

Otro beneficio es la simplicidad. Es bastante fácil entender cómo se evalúa la expresión: utiliza el alcance léxico cuando se define la función. Si fueran de otra manera, el alcance léxico podría cambiar entre la definición y la invocación y dificultar un poco la depuración. Python hace mucho para ser extremadamente directo en esos casos.

Stefan Kanev
fuente
3
Punto interesante, aunque generalmente es el principio de menor sorpresa con Python. Algunas cosas son simples en un sentido formal de complejidad del modelo, pero no obvias y sorprendentes, y creo que esto cuenta.
Steve314
1
Lo que menos sorprende aquí es esto: si tiene la semántica de evaluación en cada llamada, puede obtener un error si el cierre cambia entre dos llamadas de función (lo cual es bastante posible). Esto puede ser más sorprendente en lugar de saber que se evalúa una vez. Por supuesto, uno puede argumentar que cuando vienes de otros idiomas, excepto la semántica de evaluar en cada llamada y esa es la sorpresa, pero puedes ver cómo funciona en ambos sentidos :)
Stefan Kanev
Buen punto sobre el alcance
0xc0de
Creo que el alcance es en realidad el bit más importante. Dado que no está restringido a constantes para el valor predeterminado, es posible que necesite variables que no estén dentro del alcance en el sitio de la llamada.
Mark Ransom
5

Una forma de decirlo es que lst.append(item) no muta el lstparámetro. lstTodavía hace referencia a la misma lista. Es solo que el contenido de esa lista ha sido mutado.

Básicamente, Python no tiene (que yo recuerde) ninguna variable constante o inmutable en absoluto, pero tiene algunos tipos constantes e inmutables. No puede modificar un valor entero, solo puede reemplazarlo. Pero puede modificar el contenido de una lista sin reemplazarla.

Al igual que un número entero, no puede modificar una referencia, solo puede reemplazarla. Pero puede modificar el contenido del objeto al que se hace referencia.

En cuanto a la creación del objeto predeterminado una vez, imagino que es principalmente como una optimización, para ahorrar en la creación de objetos y los gastos generales de recolección de basura.

Steve314
fuente
+1 exactamente. Es importante comprender la capa de indirección: que una variable no es un valor; en su lugar, hace referencia a un valor. Para cambiar una variable, el valor puede intercambiarse o mutarse (si es mutable).
Joonas Pulakka
Cuando me enfrento a algo complicado que involucra variables en python, me resulta útil considerar "=" como el "operador de enlace de nombre"; el nombre siempre es rebote, ya sea que lo que estamos vinculando sea nuevo (objeto nuevo o instancia de tipo inmutable) o no.
StarWeaver
4

¿Qué beneficios ofrece este comportamiento sobre una inicialización en cada llamada?

Le permite seleccionar el comportamiento que desea, como lo demostró en su ejemplo. Entonces, si desea que el argumento predeterminado sea inmutable, utilice un valor inmutable , como Noneo 1. Si desea hacer que el argumento predeterminado sea mutable, use algo mutable, como []. Es solo flexibilidad, aunque es cierto, puede morder si no lo sabes.

Joonas Pulakka
fuente
2

Creo que la respuesta real es: Python fue escrito como un lenguaje de procedimiento y solo adoptó aspectos funcionales después del hecho. Lo que está buscando es que el valor predeterminado de los parámetros se realice como un cierre, y los cierres en Python están realmente a medias. Para evidencia de este intento:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

lo que da [2, 2, 2]donde esperarías que se produzca un verdadero cierre [0, 1, 2].

Hay muchas cosas que me gustaría si Python tuviera la capacidad de ajustar el valor predeterminado de los parámetros en los cierres. Por ejemplo:

def foo(a, b=a.b):
    ...

Sería extremadamente útil, pero "a" no está en el alcance en el momento de la definición de la función, por lo que no puede hacer eso y en su lugar debe hacer lo torpe:

def foo(a, b=None):
    if b is None:
        b = a.b

Lo cual es casi lo mismo ... casi.

ajscogo
fuente
1

Un gran beneficio es la memorización. Este es un ejemplo estándar:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

y para comparar:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Mediciones de tiempo en ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s
steffen
fuente
0

Ocurre porque la compilación en Python se realiza ejecutando el código descriptivo.

Si uno dijera

def f(x = {}):
    ....

sería bastante claro que cada vez querías una nueva matriz.

Pero y si digo:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

Aquí supongo que quiero crear cosas en varias listas, y tener un único conjunto global cuando no especifique una lista.

Pero, ¿cómo adivinaría esto el compilador? Entonces, ¿por qué intentarlo? Podríamos confiar en si se nombró o no, y a veces podría ayudar, pero en realidad solo sería adivinar. Al mismo tiempo, hay una buena razón para no intentarlo: consistencia.

Tal como está, Python simplemente ejecuta el código. A la variable list_of_all ya se le asignó un objeto, por lo que ese objeto se pasa por referencia al código que por defecto es x de la misma manera que una llamada a cualquier función obtendría una referencia a un objeto local nombrado aquí.

Si quisiéramos distinguir el caso sin nombre del caso con nombre, eso implicaría que el código en la compilación ejecuta la asignación de una manera significativamente diferente de la que se ejecuta en tiempo de ejecución. Entonces no hacemos el caso especial.

Jon Jay Obermark
fuente
-5

Esto sucede porque las funciones en Python son objetos de primera clase :

Los valores de los parámetros predeterminados se evalúan cuando se ejecuta la definición de la función. Esto significa que la expresión se evalúa una vez , cuando se define la función, y que se utiliza el mismo valor "precalculado" para cada llamada .

Continúa explicando que la edición del valor del parámetro modifica el valor predeterminado para llamadas posteriores, y que una solución simple de usar None como valor predeterminado, con una prueba explícita en el cuerpo de la función, es todo lo que se necesita para garantizar que no haya sorpresas.

Lo que significa que se def foo(l=[])convierte en una instancia de esa función cuando se llama, y ​​se reutiliza para otras llamadas. Piense en los parámetros de la función como algo que se separa de los atributos de un objeto.

Los profesionales podrían incluir aprovechar esto para que las clases tengan variables estáticas tipo C. Por lo tanto, es mejor declarar los valores predeterminados Ninguno e inicializarlos según sea necesario:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

rendimientos:

[5] [5] [5] [5]

en lugar de lo inesperado:

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]

invertir
fuente
55
No. Puede definir funciones (de primera clase o no) de manera diferente para evaluar la expresión de argumento predeterminada nuevamente para cada llamada. Y todo después de eso, es decir, aproximadamente el 90% de la respuesta, está completamente al lado de la pregunta. -1
1
Entonces, comparta con nosotros este conocimiento sobre cómo definir funciones para evaluar el argumento predeterminado para cada llamada, me gustaría saber de una manera más simple de lo que recomienda Python Docs .
invertir el
2
En un nivel de diseño de lenguaje quiero decir. La definición del lenguaje Python actualmente establece que los argumentos predeterminados se tratan de la forma en que son; igualmente podría indicar que los argumentos predeterminados se tratan de alguna otra manera. IOW está respondiendo "así son las cosas" a la pregunta "por qué las cosas son como son".
Python podría haber implementado parámetros predeterminados similares a cómo lo hace Coffeescript. Insertaría bytecode para verificar los parámetros faltantes, y si faltan evaluaría la expresión.
Winston Ewert