¿Por qué la sintaxis de la "corrección de argumento predeterminada mutable" es tan fea? Pregunta Python Newbie
names (8)
¿Qué sucedería si no estuviera hablando de listas, sino de AwesomeSets, una clase que acaba de definir? ¿Quieres definir ".local" en cada clase ?
class Foo(object):
def get(self):
return Foo()
local = property(get)
Podría funcionar, pero envejecería muy rápido, muy pronto. Muy pronto, el patrón "if a is None: a = CorrectObject ()" se convierte en una segunda naturaleza, y no lo encontrarás feo, lo encontrarás iluminador.
El problema no es de sintaxis, sino de semántica: los valores de los parámetros predeterminados se evalúan en el momento de la definición de la función, no en el tiempo de ejecución de la función.
Ahora sigo mi serie de "preguntas para principiantes de Python" y se basa en otra pregunta .
Prerrogativa
Vaya a http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables y desplácese hacia abajo hasta "Valores de parámetros predeterminados". Allí puedes encontrar lo siguiente:
def bad_append(new_item, a_list=[]):
a_list.append(new_item)
return a_list
def good_append(new_item, a_list=None):
if a_list is None:
a_list = []
a_list.append(new_item)
return a_list
Incluso hay una "Advertencia importante" en python.org con este mismo ejemplo, pero en realidad no dice que sea "mejor".
Una forma de decirlo
Entonces, la pregunta aquí es: ¿por qué la sintaxis "buena" sobre un problema conocido es fea en un lenguaje de programación que promueve la "sintaxis elegante" y la "facilidad de uso" ?
editar:
Otra forma de decirlo.
No pregunto por qué o cómo sucede (gracias Mark por el enlace).
Estoy preguntando por qué no hay una alternativa más simple incorporada en el lenguaje .
Creo que una mejor manera probablemente sería poder hacer algo en la propia def
, en la que el argumento del nombre se adjuntaría a un objeto "local" o "nuevo" dentro de la def
, objeto mutable. Algo como:
def better_append(new_item, a_list=immutable([])):
a_list.append(new_item)
return a_list
Estoy seguro de que alguien puede tener una mejor sintaxis, pero también creo que debe haber una muy buena explicación de por qué no se ha hecho esto.
Creo que estás confundiendo la sintaxis elegante con el azúcar sintáctico. La sintaxis de Python comunica ambos enfoques claramente, simplemente sucede que el enfoque correcto parece menos elegante (en términos de líneas de sintaxis) que el enfoque incorrecto. Pero como el enfoque incorrecto es bueno ... incorrecto, su elegancia es irrelevante. En cuanto a por qué algo como usted se demuestra en better_append no está implementado, supongo que There should be one-- and preferably only one --obvious way to do it.
Triunfa las pequeñas ganancias en elegancia.
El caso de uso extremadamente específico de una función que le permite pasar opcionalmente una lista para modificar, pero genera una nueva lista a menos que específicamente pase una, definitivamente no vale la pena una sintaxis de caso especial. En serio, si está realizando una serie de llamadas a esta función, ¿por qué querría hacer un caso especial en la primera llamada de la serie (pasando solo un argumento) para distinguirla de todas las demás (que necesitarán dos argumentos)? para poder seguir enriqueciendo una lista existente) ?! Por ejemplo, considere algo como (suponiendo que, por supuesto, una mejor betterappend
hizo algo útil, porque en el ejemplo actual sería una locura llamarlo en lugar de un .append
! -) directo:
def thecaller(n):
if fee(0):
newlist = betterappend(foo())
else:
newlist = betterappend(fie())
for x in range(1, n):
if fee(x):
betterappend(foo(), newlist)
else:
betterappend(fie(), newlist)
esto es simplemente una locura, y obviamente debería ser, en cambio,
def thecaller(n):
newlist = []
for x in range(n):
if fee(x):
betterappend(foo(), newlist)
else:
betterappend(fie(), newlist)
Siempre utilizando dos argumentos, evitando la repetición y construyendo una lógica mucho más simple.
La introducción de la sintaxis de casos especiales alienta y respalda el caso de uso de caja especial, y realmente no tiene mucho sentido alentar y apoyar este extremadamente peculiar: la sintaxis existente, perfectamente regular, está bien para los usos extremadamente raros del caso de uso. ).
Esto es mejor que good_append (), IMO:
def ok_append(new_item, a_list=None):
return a_list.append(new_item) if a_list else [ new_item ]
También puedes tener mucho cuidado y verificar que a_list sea una lista ...
Esto se llama la ''trampa de valores por defecto mutable''. Consulte: http://www.ferg.org/projects/python_gotchas.html#contents_item_6
Básicamente, a_list
se inicializa cuando el programa se interpreta por primera vez, no cada vez que llama a la función (como podría esperarse de otros idiomas). Por lo tanto, no recibe una nueva lista cada vez que llama a la función, pero está reutilizando la misma.
Supongo que la respuesta a la pregunta es que si desea agregar algo a una lista, simplemente hágalo, no cree una función para hacerlo.
Esta:
>>> my_list = []
>>> my_list.append(1)
Es más claro y fácil de leer que:
>>> my_list = my_append(1)
En el caso práctico, si necesitara este tipo de comportamiento, probablemente crearía su propia clase que tiene métodos para administrar su lista interna.
He editado esta respuesta para incluir pensamientos de los muchos comentarios publicados en la pregunta.
El ejemplo que das es defectuoso . Modifica la lista que le pasas como efecto secundario. Si así es como pretendía que funcionara la función, no tendría sentido tener un argumento predeterminado. Tampoco tendría sentido devolver la lista actualizada. Sin un argumento por defecto, el problema desaparece.
Si la intención era devolver una nueva lista, necesita hacer una copia de la lista de entrada. Python prefiere que las cosas sean explícitas, por lo que depende de usted hacer la copia.
def better_append(new_item, a_list=[]):
new_list = list(a_list)
new_list.append(new_item)
return new_list
Para algo un poco diferente, puede hacer un generador que pueda tomar una lista o un generador como entrada:
def generator_append(new_item, a_list=[]):
for x in a_list:
yield x
yield new_item
Creo que está bajo la idea errónea de que Python trata los argumentos predeterminados mutables e inmutables de manera diferente; eso simplemente no es verdad Más bien, la inmutabilidad del argumento te hace cambiar tu código de una manera sutil para hacer lo correcto automáticamente. Tome su ejemplo y haga que se aplique a una cadena en lugar de a una lista:
def string_append(new_item, a_string=''''):
a_string = a_string + new_item
return a_string
Este código no cambia la cadena pasada, no puede, porque las cadenas son inmutables. Crea una nueva cadena y asigna una cadena a esa nueva cadena. El argumento predeterminado se puede usar una y otra vez porque no cambia, al principio hiciste una copia.
Los argumentos predeterminados se evalúan en el momento en que se ejecuta la instrucción def
, que es probablemente el enfoque más razonable: a menudo es lo que se desea. Si no fuera así, podría causar resultados confusos cuando el entorno cambia un poco.
La diferenciación con un método local
mágico o algo así está lejos de ser ideal. Python trata de dejar las cosas bastante claras y no hay un reemplazo obvio y claro para el modelo actual que no recurra a meterse con la semántica bastante consistente que Python tiene actualmente.
Probablemente no deberías definir estas dos funciones como buenas y malas. Puede usar el primero con la lista o los diccionarios para implementar modificaciones de lugar de los objetos correspondientes. En mi opinión, este método puede causarle dolores de cabeza si no sabe cómo funcionan los objetos mutables, pero dado que sabe lo que está haciendo, está bien.
Así que tienes dos métodos diferentes para pasar parámetros que proporcionan diferentes comportamientos. Y esto es bueno, no lo cambiaría.