python python-3.x

python - Concatenación del resultado de una función con un argumento predeterminado mutable



python-3.x (2)

Supongamos una función con un argumento predeterminado mutable:

def f(l=[]): l.append(len(l)) return l

Si ejecuto esto:

def f(l=[]): l.append(len(l)) return l print(f()+["-"]+f()+["-"]+f()) # -> [0, ''-'', 0, 1, ''-'', 0, 1, 2]

O esto:

def f(l=[]): l.append(len(l)) return l print(f()+f()+f()) # -> [0, 1, 0, 1, 0, 1, 2]

En lugar del siguiente, lo que sería más lógico:

print(f()+f()+f()) # -> [0, 0, 1, 0, 1, 2]

¿Por qué?


Aquí hay una manera de pensarlo que podría ayudarlo a tener sentido:

Una función es una estructura de datos . Creas uno con un bloque def , de la misma manera que creas un tipo con un bloque de class o creas una lista entre corchetes.

La parte más interesante de esa estructura de datos es el código que se ejecuta cuando se llama a la función, ¡pero los argumentos predeterminados también son parte de ella! De hecho, puede inspeccionar tanto el código como los argumentos predeterminados de Python, a través de atributos en la función:

>>> def foo(a=1): pass ... >>> dir(foo) [''__annotations__'', ''__call__'', ''__class__'', ''__closure__'', ''__code__'', ''__defaults__'', ...] >>> foo.__code__ <code object foo at 0x7f114752a660, file "<stdin>", line 1> >>> foo.__defaults__ (1,)

(Una interfaz mucho mejor para esto es inspect.signature , pero todo lo que hace es examinar esos atributos ).

Entonces, la razón por la que esto modifica la lista:

def f(l=[]): l.append(len(l)) return l

es exactamente la misma razón por la que esto también modifica la lista:

f = dict(l=[]) f[''l''].append(len(f[''l'']))

En ambos casos, está mutando una lista que pertenece a alguna estructura principal, por lo que el cambio también será visible naturalmente en el elemento principal.

Tenga en cuenta que esta es una decisión de diseño que Python tomó específicamente, y no es inherentemente necesaria en un lenguaje. JavaScript aprendió recientemente acerca de los argumentos predeterminados, pero los trata como expresiones para ser reevaluados nuevamente en cada llamada; esencialmente, cada argumento predeterminado es su propia función pequeña. La ventaja es que JS no tiene este problema, pero el inconveniente es que no puede inspeccionar significativamente los valores predeterminados de la manera que puede hacerlo en Python.


Eso es realmente bastante interesante!

Como sabemos, la lista l en la definición de la función se inicializa solo una vez en la definición de esta función, y para todas las invocaciones de esta función, habrá exactamente una copia de esta lista. Ahora, la función modifica esta lista, lo que significa que varias llamadas a esta función modificarán exactamente el mismo objeto varias veces. Esta es la primera parte importante.

Ahora, considere la expresión que agrega estas listas:

f()+f()+f()

De acuerdo con las leyes de precedencia de operadores, esto es equivalente a lo siguiente:

(f() + f()) + f()

... que es exactamente lo mismo que esto:

temp1 = f() + f() # (1) temp2 = temp1 + f() # (2)

Esta es la segunda parte importante.

La adición de listas produce un nuevo objeto, sin modificar ninguno de sus argumentos. Esta es la tercera parte importante.

Ahora combinemos lo que sabemos juntos.

En la línea 1 anterior, la primera llamada devuelve [0] , como era de esperar. La segunda llamada devuelve [0, 1] , como era de esperar. ¡Oh espera! ¡La función devolverá exactamente el mismo objeto (¡no su copia!) Una y otra vez, después de modificarlo! ¡Esto significa que el objeto que devolvió la primera llamada ahora también ha cambiado para convertirse en [0, 1] ! Y es por eso que temp1 == [0, 1] + [0, 1] .

Sin embargo, el resultado de la suma es un objeto completamente nuevo , por lo que [0, 1, 0, 1] + f() es lo mismo que [0, 1, 0, 1] + [0, 1, 2] . Tenga en cuenta que la segunda lista es, nuevamente, exactamente lo que esperaría que devolviera su función. Lo mismo sucede cuando agrega f() + ["-"] : esto crea un nuevo objeto de list , para que cualquier otra llamada a f no interfiera con él.

Puede reproducir esto concatenando los resultados de dos llamadas a funciones:

>>> f() + f() [0, 1, 0, 1] >>> f() + f() [0, 1, 2, 3, 0, 1, 2, 3] >>> f() + f() [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5]

Una vez más, puede hacer todo eso porque está concatenando referencias al mismo objeto .