Cualquiera que haya jugado con Python el tiempo suficiente ha sido mordido (o despedazado) por el siguiente problema:
def foo(a=[]):
a.append(5)
return a
Principiantes de Python que se esperan esta función para volver siempre una lista con un solo elemento: [5]
. El resultado es, en cambio, muy diferente y muy sorprendente (para un novato):
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()
Un gerente mío tuvo una vez su primer encuentro con esta característica, y lo calificó como "una falla dramática en el diseño" del lenguaje. Respondí que el comportamiento tenía una explicación subyacente, y de hecho es muy desconcertante e inesperado si no entiendes lo interno. Sin embargo, no pude responder (a mí mismo) la siguiente pregunta: ¿cuál es la razón para vincular el argumento predeterminado en la definición de la función y no en la ejecución de la función? Dudo que el comportamiento experimentado tenga un uso práctico (¿quién realmente usó variables estáticas en C, sin criar insectos?)
Editar :
Baczek hizo un ejemplo interesante. Junto con la mayoría de sus comentarios y los de Utaal en particular, elaboré más:
>>> def a():
... print("a executed")
... return []
...
>>>
>>> def b(x=a()):
... x.append(5)
... print(x)
...
a executed
>>> b()
[5]
>>> b()
[5, 5]
Para mí, parece que la decisión de diseño fue relativa a dónde poner el alcance de los parámetros: dentro de la función o "junto" con ella?
Hacer el enlace dentro de la función significaría que x
está efectivamente vinculado al valor predeterminado especificado cuando se llama a la función, no está definida, algo que presentaría un defecto profundo: la def
línea sería "híbrida" en el sentido de que parte del enlace (de el objeto de función) sucedería en la definición, y parte (asignación de parámetros predeterminados) en el momento de invocación de la función.
El comportamiento real es más consistente: todo lo de esa línea se evalúa cuando se ejecuta esa línea, es decir, en la definición de la función.
fuente
[5]
". Soy un principiante de Python, y yo no esperaría que esto, porque, obviamente,foo([1])
va a volver[1, 5]
, no[5]
. Lo que querías decir es que un novato esperaría que la función llamada sin parámetro siempre regrese[5]
.Respuestas:
En realidad, esto no es un defecto de diseño, y no se debe a aspectos internos o de rendimiento.
Viene simplemente del hecho de que las funciones en Python son objetos de primera clase, y no solo un fragmento de código.
Tan pronto como llegue a pensar de esta manera, tiene sentido completamente: una función es un objeto que se evalúa en su definición; los parámetros predeterminados son una especie de "datos de miembros" y, por lo tanto, su estado puede cambiar de una llamada a otra, exactamente como en cualquier otro objeto.
En cualquier caso, Effbot tiene una muy buena explicación de las razones de este comportamiento en los valores de parámetros predeterminados en Python .
Lo encontré muy claro, y realmente sugiero leerlo para conocer mejor cómo funcionan los objetos de función.
fuente
functions are objects
. En su paradigma, la propuesta sería implementar los valores predeterminados de las funciones como propiedades en lugar de atributos.Supongamos que tiene el siguiente código
Cuando veo la declaración de comer, lo menos sorprendente es pensar que si no se da el primer parámetro, será igual a la tupla
("apples", "bananas", "loganberries")
Sin embargo, supuestamente más adelante en el código, hago algo como
entonces, si los parámetros predeterminados se vincularon en la ejecución de la función en lugar de la declaración de la función, me sorprendería (de una manera muy mala) descubrir que las frutas habían cambiado. Esto sería más sorprendente IMO que descubrir que su
foo
función anterior estaba mutando la lista.El verdadero problema radica en las variables mutables, y todos los idiomas tienen este problema hasta cierto punto. Aquí hay una pregunta: supongamos que en Java tengo el siguiente código:
Ahora, ¿utiliza mi mapa el valor de la
StringBuffer
clave cuando se colocó en el mapa, o almacena la clave por referencia? De cualquier manera, alguien está asombrado; o la persona que trató de sacar el objetoMap
usando un valor idéntico al que lo colocó, o la persona que parece no poder recuperar su objeto a pesar de que la clave que está usando es literalmente el mismo objeto eso se usó para ponerlo en el mapa (esta es la razón por la cual Python no permite que sus tipos de datos incorporados mutables se usen como claves de diccionario).Su ejemplo es un buen caso en el que los recién llegados a Python serán sorprendidos y mordidos. Pero yo diría que si "solucionáramos" esto, entonces eso solo crearía una situación diferente en la que serían mordidos, y esa sería aún menos intuitiva. Además, este es siempre el caso cuando se trata de variables mutables; siempre te encuentras con casos en los que alguien puede esperar intuitivamente uno o el comportamiento opuesto dependiendo del código que esté escribiendo.
Personalmente, me gusta el enfoque actual de Python: los argumentos de la función predeterminada se evalúan cuando se define la función y ese objeto es siempre el predeterminado. Supongo que podrían usar mayúsculas y minúsculas usando una lista vacía, pero ese tipo de carcasa especial causaría aún más asombro, sin mencionar que sería incompatible al revés.
fuente
("blueberries", "mangos")
.some_random_function()
APPENDs alfruits
lugar de asignar a la misma, el comportamiento deeat()
van a cambiar. Esto en cuanto al maravilloso diseño actual. Si utiliza un argumento predeterminado al que se hace referencia en otro lugar y luego modifica la referencia desde fuera de la función, está solicitando problemas. El verdadero WTF es cuando las personas definen un nuevo argumento por defecto (un literal de lista o una llamada a un constructor), y aún así obtienen un poco.global
y reasignar explícitamente la tupla: no hay absolutamente nada sorprendente sieat
funciona de manera diferente después de eso.La parte relevante de la documentación :
fuente
function.data = []
) o, mejor aún, hacer un objeto.No sé nada sobre el funcionamiento interno del intérprete de Python (y tampoco soy un experto en compiladores e intérpretes), así que no me culpen si propongo algo insensible o imposible.
Siempre que los objetos de Python sean mutables , creo que esto debe tenerse en cuenta al diseñar los elementos de argumentos predeterminados. Cuando crea una instancia de una lista:
espera obtener una nueva lista referenciada por
a
.¿Por qué debería el
a=[]
eninstanciar una nueva lista en la definición de la función y no en la invocación? Es como si estuviera preguntando "si el usuario no proporciona el argumento, cree una nueva lista y úsela como si hubiera sido producida por la persona que llama". Creo que esto es ambiguo en su lugar:
usuario, ¿desea
a
predeterminar la fecha y hora correspondiente a cuando está definiendo o ejecutandox
? En este caso, como en el anterior, mantendré el mismo comportamiento que si el argumento predeterminado "asignación" fuera la primera instrucción de la función (datetime.now()
llamada a la invocación de la función). Por otro lado, si el usuario quisiera el mapeo del tiempo de definición, podría escribir:Lo sé, lo sé: eso es un cierre. Alternativamente, Python podría proporcionar una palabra clave para forzar el enlace de tiempo de definición:
fuente
class {}
bloque se interpreten como pertenecientes a las instancias :) Pero cuando las clases son objetos de primera clase, obviamente lo natural es que sus contenidos (en la memoria) reflejen sus contenidos (en codigo).Bueno, la razón es simplemente que los enlaces se realizan cuando se ejecuta el código, y la definición de la función se ejecuta, bueno ... cuando se definen las funciones.
Compara esto:
Este código sufre exactamente la misma casualidad inesperada. bananas es un atributo de clase y, por lo tanto, cuando le agrega cosas, se agrega a todas las instancias de esa clase. La razón es exactamente la misma.
Es solo "Cómo funciona", y hacer que funcione de manera diferente en el caso de la función probablemente sería complicado, y en el caso de la clase probablemente imposible, o al menos ralentizaría mucho la creación de instancias de objetos, ya que tendría que mantener el código de la clase alrededor y ejecutarlo cuando se crean objetos.
Si, es inesperado. Pero una vez que el centavo cae, encaja perfectamente con cómo funciona Python en general. De hecho, es una buena ayuda para la enseñanza, y una vez que comprenda por qué sucede esto, comprenderá Python mucho mejor.
Dicho esto, debería ocupar un lugar destacado en cualquier buen tutorial de Python. Porque como mencionas, todos se encuentran con este problema tarde o temprano.
fuente
property
s, que en realidad son funciones de nivel de clase que actúan como atributos normales pero guardan el atributo en la instancia en lugar de la clase (usandoself.attribute = value
como dijo Lennart).¿Por qué no introspectivas?
Estoy realmente sorprendido de que nadie haya realizado la perspicaz introspección ofrecida por Python (
2
y3
aplicar) en las llamadas.Dada una pequeña función simple
func
definida como:Cuando Python lo encuentra, lo primero que hará es compilarlo para crear un
code
objeto para esta función. Mientras se realiza este paso de compilación, Python evalúa * y luego almacena los argumentos predeterminados (una lista vacía[]
aquí) en el propio objeto de función . Como se menciona en la respuesta principal: la listaa
ahora puede considerarse un miembro de la funciónfunc
.Entonces, hagamos una introspección, un antes y un después para examinar cómo se expande la lista dentro del objeto de función. Estoy usando
Python 3.x
para esto, para Python 2 lo mismo se aplica (uso__defaults__
ofunc_defaults
en Python 2; sí, dos nombres para la misma cosa).Función antes de la ejecución:
Después de que Python ejecute esta definición, tomará los parámetros predeterminados especificados (
a = []
aquí) y los agrupará en el__defaults__
atributo para el objeto de función (sección relevante: Callables):Ok, entonces una lista vacía como entrada única
__defaults__
, tal como se esperaba.Función después de la ejecución:
Ahora ejecutemos esta función:
Ahora, veamos esos de
__defaults__
nuevo:¿Asombrado? ¡El valor dentro del objeto cambia! Las llamadas consecutivas a la función ahora simplemente se agregarán a ese
list
objeto incrustado :Entonces, ahí lo tiene, la razón por la que ocurre este 'defecto' es porque los argumentos predeterminados son parte del objeto de función. Aquí no pasa nada extraño, todo es un poco sorprendente.
La solución común para combatir esto es usar
None
como predeterminado y luego inicializar en el cuerpo de la función:Dado que el cuerpo de la función se ejecuta de nuevo cada vez, siempre se obtiene una nueva lista vacía nueva si no se pasó ningún argumento
a
.Para verificar aún más que la lista en
__defaults__
es la misma que la utilizada en la funciónfunc
, simplemente puede cambiar su función para devolverid
la listaa
utilizada dentro del cuerpo de la función. Luego, compárelo con la lista en__defaults__
(posición[0]
en__defaults__
) y verá cómo se refieren a la misma instancia de lista:¡Todo con el poder de la introspección!
* Para verificar que Python evalúa los argumentos predeterminados durante la compilación de la función, intente ejecutar lo siguiente:
como notará,
input()
se llama antes del proceso de construcción de la función y se vincula al nombrebar
.fuente
id(...)
necesario para esa última verificación, o elis
operador respondería la misma pregunta?is
estaría bien, solo lo uséid(val)
porque creo que podría ser más intuitivo.None
como predeterminado limita severamente la utilidad de la__defaults__
introspección, por lo que no creo que funcione bien como una defensa de tener el__defaults__
trabajo como lo hace. La evaluación diferida haría más para mantener útiles los valores predeterminados de las funciones de ambos lados.Solía pensar que crear los objetos en tiempo de ejecución sería el mejor enfoque. Ahora estoy menos seguro, ya que pierdes algunas características útiles, aunque puede valer la pena, independientemente de simplemente evitar la confusión del novato. Las desventajas de hacerlo son:
1. Rendimiento
Si se usa la evaluación del tiempo de llamada, entonces se llama a la función costosa cada vez que se usa su función sin un argumento. Pagaría un precio costoso en cada llamada o necesitaría almacenar manualmente el valor en la memoria externa, contaminando su espacio de nombres y agregando verbosidad.
2. Forzar parámetros vinculados
Un truco útil es vincular los parámetros de un lambda al enlace actual de una variable cuando se crea el lambda. Por ejemplo:
Esto devuelve una lista de funciones que devuelven 0,1,2,3 ... respectivamente. Si se cambia el comportamiento, en su lugar se unirán
i
al valor de tiempo de llamada de i, por lo que obtendría una lista de funciones que todos devuelven9
.La única forma de implementar esto de otra manera sería crear un cierre adicional con el límite i, es decir:
3. Introspección
Considera el código:
Podemos obtener información sobre los argumentos y valores predeterminados utilizando el
inspect
módulo, queEsta información es muy útil para cosas como la generación de documentos, metaprogramación, decoradores, etc.
Ahora, suponga que el comportamiento de los valores predeterminados podría modificarse para que esto sea equivalente a:
Sin embargo, hemos perdido la capacidad de introspección y ver cuáles son los argumentos predeterminados . Debido a que los objetos no se han construido, no podemos obtenerlos sin llamar a la función. Lo mejor que podemos hacer es almacenar el código fuente y devolverlo como una cadena.
fuente
5 puntos en defensa de Python
Simplicidad : el comportamiento es simple en el siguiente sentido: la mayoría de las personas caen en esta trampa solo una vez, no varias.
Consistencia : Python siempre pasa objetos, no nombres. El parámetro predeterminado es, obviamente, parte del encabezado de la función (no el cuerpo de la función). Por lo tanto, debe evaluarse en el tiempo de carga del módulo (y solo en el tiempo de carga del módulo, a menos que esté anidado), no en el momento de la llamada a la función.
Utilidad : Como señala Frederik Lundh en su explicación de "Valores de parámetros predeterminados en Python" , el comportamiento actual puede ser bastante útil para la programación avanzada. (Utilizar con moderación.)
Documentación suficiente : en la documentación más básica de Python, el tutorial, el problema se anuncia en voz alta como una "Advertencia importante" en la primera subsección de la Sección "Más sobre la definición de funciones" . La advertencia incluso usa negrita, que rara vez se aplica fuera de los encabezados. RTFM: lea el excelente manual.
Meta-aprendizaje : caer en la trampa es en realidad un momento muy útil (al menos si eres un estudiante reflexivo), porque posteriormente comprenderás mejor el punto "Consistencia" anterior y eso te enseñará mucho sobre Python.
fuente
Este comportamiento se explica fácilmente por:
Entonces:
a
no cambia - cada llamada de asignación crea un nuevo objeto int - se imprime un nuevo objetob
no cambia: la nueva matriz se crea a partir del valor predeterminado y se imprimec
cambios - la operación se realiza en el mismo objeto - y se imprimefuente
__iadd__
, pero no funciona con int. Por supuesto. :-)Lo que estás preguntando es por qué esto:
no es internamente equivalente a esto:
a excepción del caso de llamar explícitamente a func (None, None), que ignoraremos.
En otras palabras, en lugar de evaluar los parámetros predeterminados, ¿por qué no almacenar cada uno de ellos y evaluarlos cuando se llama a la función?
Probablemente haya una respuesta: convertiría efectivamente todas las funciones con parámetros predeterminados en un cierre. Incluso si todo está oculto en el intérprete y no un cierre completo, los datos deben almacenarse en algún lugar. Sería más lento y usaría más memoria.
fuente
1) El llamado problema del "Argumento predeterminado mutable" es en general un ejemplo especial que demuestra que:
"Todas las funciones con este problema también sufren un problema de efectos secundarios similar en el parámetro real ",
eso va en contra de las reglas de la programación funcional, por lo general, no se puede despreciar y se deben arreglar juntos.
Ejemplo:
Solución : una copia
una solución absolutamente seguro es que
copy
nideepcopy
el objeto de entrada primero y luego hacer lo que sea con la copia.Muchos tipos mutables incorporados tienen un método de copia como
some_dict.copy()
osome_set.copy()
, o se pueden copiar fácilmente comosomelist[:]
olist(some_list)
. Cada objeto también puede ser copiadocopy.copy(any_object)
o más completo porcopy.deepcopy()
(este último útil si el objeto mutable está compuesto de objetos mutables). Algunos objetos se basan fundamentalmente en efectos secundarios como el objeto "archivo" y no se pueden reproducir de manera significativa mediante copia. proceso de copiarProblema de ejemplo para una pregunta SO similar
No se debe guardar ni guardar en ninguna atributo público de una instancia devuelta por esta función. (Suponiendo que los atributos privados de instancia no se deben modificar desde fuera de esta clase o subclases por convención. Es decir,
_var1
es un atributo privado)Conclusión:
objetos de parámetros de entrada no deben modificarse en su lugar (mutar) ni tampoco deben vincularse a un objeto devuelto por la función. (Si preferimos la programación sin efectos secundarios, lo cual es muy recomendable. Ver Wiki sobre "efectos secundarios" (Los dos primeros párrafos son relevantes en este contexto).)
2)
Solo si el efecto secundario sobre el parámetro real es requerido pero no deseado en el parámetro predeterminado, entonces la solución útil es
def ...(var1=None):
if var1 is None:
var1 = []
Más ...3) En algunos casos es útil el comportamiento mutable de los parámetros predeterminados .
fuente
def f( a = None )
se recomienda la construcción cuando realmente quieres decir algo más. Copiar está bien, porque no debes mutar los argumentos. Y cuando lo hacesif a is None: a = [1, 2, 3]
, copias la lista de todos modos.En realidad, esto no tiene nada que ver con los valores predeterminados, aparte de que a menudo aparece como un comportamiento inesperado al escribir funciones con valores predeterminados mutables.
No hay valores predeterminados a la vista en este código, pero obtiene exactamente el mismo problema.
El problema es que
foo
está modificando una variable mutable transmitida por la persona que llama, cuando la persona que llama no espera esto. Un código como este estaría bien si la función se llamara algo asíappend_5
; entonces la persona que llama llamaría a la función para modificar el valor que pasa y se esperaría el comportamiento. Pero es muy poco probable que una función de este tipo tome un argumento predeterminado, y probablemente no devolverá la lista (ya que la persona que llama ya tiene una referencia a esa lista; la que acaba de pasar).Su original
foo
, con un argumento predeterminado, no debería modificara
si se pasó explícitamente o si obtuvo el valor predeterminado. Su código debe dejar solo argumentos mutables a menos que sea claro por el contexto / nombre / documentación que se supone que los argumentos deben ser modificados. Usar valores mutables pasados como argumentos como temporarios locales es una idea extremadamente mala, ya sea que estemos en Python o no y si hay argumentos predeterminados involucrados o no.Si necesita manipular destructivamente un temporal local en el curso de calcular algo, y necesita comenzar su manipulación desde un valor de argumento, debe hacer una copia.
fuente
append
cambiara
"en el lugar"). Que un mutable predeterminado no se vuelva a instanciar en cada llamada es el bit "inesperado" ... al menos para mí. :)cache={}
. Sin embargo, sospecho que este "menor asombro" surge cuando no esperas (o no quieres) la función que estás llamando para silenciar el argumento.cache={}
para completar.None
y la asignación del valor predeterminado real si el argumento argNone
no resuelve ese problema (lo considero un antipatrón por ese motivo). Si corrige el otro error evitando los valores de argumento de mutación, ya sea que tengan valores predeterminados o no, entonces nunca notará ni se preocupará por este comportamiento "asombroso".Tema ya ocupado, pero por lo que leí aquí, lo siguiente me ayudó a darme cuenta de cómo funciona internamente:
fuente
a = a + [1]
sobrecargasa
... considere cambiarlob = a + [1] ; print id(b)
y agregar una líneaa.append(2)
. Eso hará que sea más obvio que+
en dos listas siempre se crea una nueva lista (asignada ab
), mientras que una modificacióna
aún puede tener la mismaid(a)
.Es una optimización de rendimiento. Como resultado de esta funcionalidad, ¿cuál de estas dos llamadas a funciones cree que es más rápida?
Te daré una pista. Aquí está el desmontaje (ver http://docs.python.org/library/dis.html ):
#
1#
2Como se puede ver, no es una ventaja en el rendimiento cuando se usan parámetros por defecto inmutables. Esto puede marcar la diferencia si es una función llamada con frecuencia o si el argumento predeterminado tarda mucho en construirse. Además, tenga en cuenta que Python no es C. En C tiene constantes que son prácticamente gratuitas. En Python no tienes este beneficio.
fuente
Python: el argumento predeterminado mutable
Los argumentos predeterminados se evalúan en el momento en que la función se compila en un objeto de función. Cuando es utilizada por la función, varias veces por esa función, son y siguen siendo el mismo objeto.
Cuando son mutables, cuando mutan (por ejemplo, al agregarle un elemento) permanecen mutados en llamadas consecutivas.
Permanecen mutados porque son el mismo objeto cada vez.
Código equivalente
Dado que la lista está vinculada a la función cuando el objeto de función se compila y se instancia, esto:
es casi exactamente equivalente a esto:
Demostración
Aquí hay una demostración: puede verificar que son el mismo objeto cada vez que los hace referencia
example.py
y ejecutarlo con
python example.py
:¿Esto viola el principio de "Menos asombro"?
Este orden de ejecución es frecuentemente confuso para los nuevos usuarios de Python. Si comprende el modelo de ejecución de Python, entonces se vuelve bastante esperado.
La instrucción habitual para los nuevos usuarios de Python:
Pero esta es la razón por la cual la instrucción habitual para los nuevos usuarios es crear sus argumentos predeterminados como este:
Esto usa el singleton None como un objeto centinela para decirle a la función si hemos recibido o no un argumento diferente al predeterminado. Si no obtenemos argumentos, en realidad queremos usar una nueva lista vacía
[]
, como valor predeterminado.Como dice la sección del tutorial sobre el flujo de control :
fuente
La respuesta más corta probablemente sería "definición es ejecución", por lo tanto, todo el argumento no tiene sentido estricto. Como un ejemplo más artificial, puedes citar esto:
Con suerte, es suficiente para mostrar que no ejecutar las expresiones de argumento predeterminadas en el momento de la ejecución de la
def
declaración no es fácil o no tiene sentido, o ambos.Sin embargo, estoy de acuerdo en que es un problema cuando intentas usar constructores predeterminados.
fuente
Una solución simple usando None
fuente
Este comportamiento no es sorprendente si tiene en cuenta lo siguiente:
El papel de (2) se ha cubierto ampliamente en este hilo. (1) es probablemente el factor causante de asombro, ya que este comportamiento no es "intuitivo" cuando proviene de otros idiomas.
(1) se describe en el tutorial de Python sobre clases . En un intento de asignar un valor a un atributo de clase de solo lectura:
Vuelva al ejemplo original y considere los puntos anteriores:
Aquí
foo
hay un objeto ya
es un atributo defoo
(disponible enfoo.func_defs[0]
). Dado quea
es una lista,a
es mutable y, por lo tanto, es un atributo de lectura-escritura defoo
. Se inicializa en la lista vacía según lo especificado por la firma cuando se instancia la función, y está disponible para leer y escribir mientras exista el objeto de función.Llamar
foo
sin anular un valor predeterminado utiliza el valor de ese valor predeterminadofoo.func_defs
. En este caso,foo.func_defs[0]
se utiliza paraa
el alcance del código del objeto de función. Cambios aa
cambiofoo.func_defs[0]
, que es parte delfoo
objeto y persiste entre la ejecución del código enfoo
.Ahora, compare esto con el ejemplo de la documentación sobre la emulación del comportamiento de argumento predeterminado de otros idiomas , de modo que los valores predeterminados de la firma de la función se usen cada vez que se ejecuta la función:
Teniendo en cuenta (1) y (2) , uno puede ver por qué esto logra el comportamiento deseado:
foo
instancia del objeto de función,foo.func_defs[0]
se establece enNone
un objeto inmutable.L
en la llamada a la función),foo.func_defs[0]
(None
) está disponible en el ámbito local comoL
.L = []
, la asignación no puede tener éxito enfoo.func_defs[0]
, porque ese atributo es de solo lectura.L
se crea en el ámbito local y se utiliza para el resto de la llamada a la función.foo.func_defs[0]
así permanece sin cambios para futuras invocaciones defoo
.fuente
Voy a demostrar una estructura alternativa para pasar un valor de lista predeterminado a una función (funciona igualmente bien con los diccionarios).
Como otros han comentado extensamente, el parámetro de lista está vinculado a la función cuando se define y no cuando se ejecuta. Debido a que las listas y los diccionarios son mutables, cualquier modificación a este parámetro afectará otras llamadas a esta función. Como resultado, las llamadas posteriores a la función recibirán esta lista compartida que puede haber sido alterada por cualquier otra llamada a la función. Peor aún, dos parámetros están utilizando el parámetro compartido de esta función al mismo tiempo ajeno a los cambios realizados por el otro.
Método incorrecto (probablemente ...) :
Puede verificar que son el mismo objeto usando
id
:Según "Python eficaz: 59 formas específicas de escribir mejor Python" de Brett Slatkin, Elemento 20: Uso
None
y cadenas de documentos para especificar argumentos dinámicos predeterminados (p. 48)Esta implementación garantiza que cada llamada a la función reciba la lista predeterminada o la lista que se pasa a la función.
Método preferido :
Puede haber casos de uso legítimos para el 'Método incorrecto' por el cual el programador pretendía que se compartiera el parámetro de lista predeterminado, pero es más probable que sea la excepción que la regla.
fuente
Las soluciones aquí son:
None
como su valor predeterminado (o un nonceobject
) y actívelo para crear sus valores en tiempo de ejecución; olambda
como parámetro predeterminado y llámelo dentro de un bloque try para obtener el valor predeterminado (este es el tipo de cosa para la que sirve la abstracción lambda).La segunda opción es buena porque los usuarios de la función pueden pasar un invocable, que ya puede existir (como a
type
)fuente
Cuando hacemos esto:
... asignamos el argumento
a
a un sin nombre lista , si la persona que llama no pasa el valor de a.Para simplificar las cosas para esta discusión, le daremos temporalmente un nombre a la lista sin nombre. ¿Qué tal
pavlo
?En cualquier momento, si la persona que llama no nos dice qué
a
es, lo reutilizamospavlo
.Si
pavlo
es mutable (modificable) yfoo
termina modificándolo, un efecto que notamos la próxima vezfoo
se llama sin especificara
.Entonces esto es lo que ves (recuerda,
pavlo
se inicializa a []):Ahora,
pavlo
es [5].Llamar
foo()
nuevamente modificapavlo
nuevamente:Especificar
a
cuándo llamarfoo()
asegurapavlo
que no se toque.Entonces,
pavlo
todavía está[5, 5]
.fuente
A veces exploto este comportamiento como una alternativa al siguiente patrón:
Si
singleton
solo lo usause_singleton
, me gusta el siguiente patrón como reemplazo:Lo he usado para crear instancias de clases de clientes que acceden a recursos externos, y también para crear dictados o listas para la memorización.
Como no creo que este patrón sea bien conocido, pongo un breve comentario para evitar futuros malentendidos.
fuente
_make_singleton
en el momento de la definición en el ejemplo de argumento predeterminado, sino en el momento de la llamada en el ejemplo global. Una sustitución verdadera usaría algún tipo de cuadro mutable para el valor de argumento predeterminado, pero la adición del argumento brinda la oportunidad de pasar valores alternativos.Puede evitar esto reemplazando el objeto (y, por lo tanto, el vínculo con el alcance):
Feo, pero funciona.
fuente
Puede ser cierto que:
es completamente consistente mantener ambas características anteriores y aún hacer otro punto:
Las otras respuestas, o al menos algunas de ellas, hacen los puntos 1 y 2 pero no 3, o hacen el punto 3 y minimizan los puntos 1 y 2. Pero las tres son verdaderas.
Puede ser cierto que cambiar caballos en la mitad de la corriente aquí requeriría una rotura significativa, y que podría haber más problemas creados al cambiar Python para manejar intuitivamente el fragmento de apertura de Stefano. Y puede ser cierto que alguien que conociera bien las partes internas de Python podría explicar un campo minado de consecuencias. Sin embargo,
El comportamiento existente no es Pythonic, y Python es exitoso porque muy poco sobre el lenguaje viola el principio de menor asombro en cualquier lugar cercanoesta mal Es un problema real, sea o no conveniente desarraigarlo. Es un defecto de diseño. Si comprende el lenguaje mucho mejor al tratar de rastrear el comportamiento, puedo decir que C ++ hace todo esto y más; aprende mucho navegando, por ejemplo, errores sutiles de puntero. Pero esto no es Pythonic: las personas que se preocupan por Python lo suficiente como para perseverar frente a este comportamiento son personas que se sienten atraídas por el lenguaje porque Python tiene muchas menos sorpresas que otro lenguaje. Dabblers y los curiosos se convierten en Pythonistas cuando se sorprenden del poco tiempo que lleva hacer que algo funcione, no por un diseño fl - quiero decir, un rompecabezas de lógica oculta - que corta en contra de las intuiciones de los programadores que se sienten atraídos por Python porque simplemente funciona .
fuente
x=[]
significa "crear un objeto de lista vacío y vincular el nombre 'x' a él". Entonces, endef f(x=[])
, también se crea una lista vacía. No siempre se vincula a x, por lo que se vincula al sustituto predeterminado. Más tarde, cuando se llama a f (), el valor predeterminado se extrae y se vincula a x. Dado que fue la lista vacía la que se retiró, esa misma lista es lo único disponible para enlazar a x, ya sea que algo se haya atascado dentro de ella o no. ¿Cómo podría ser de otra manera?Esto no es un defecto de diseño . Cualquiera que tropiece con esto está haciendo algo mal.
Hay 3 casos en los que puedo encontrar este problema:
cache={}
, y no se espera que llame a la función con un argumento real.El ejemplo en la pregunta podría caer en la categoría 1 o 3. Es extraño que ambos modifiquen la lista aprobada y la devuelvan; deberías elegir uno u otro.
fuente
cache={}
patrón es realmente una solución de solo entrevista, en el código real que probablemente quieras@lru_cache
!¡Este "error" me dio muchas horas extra de trabajo! Pero estoy empezando a ver un uso potencial de él (pero me hubiera gustado que estuviera en el momento de la ejecución, aún)
Te daré lo que veo como un ejemplo útil.
imprime lo siguiente
fuente
Simplemente cambie la función para que sea:
fuente
Creo que la respuesta a esta pregunta radica en cómo Python pasa los datos al parámetro (pase por valor o por referencia), no en la mutabilidad o cómo Python maneja la declaración "def".
Una breve introduccion. Primero, hay dos tipos de tipos de datos en python, uno es un tipo de datos elemental simple, como los números, y otro tipo de datos son los objetos. En segundo lugar, al pasar datos a parámetros, python pasa el tipo de datos elemental por valor, es decir, hace una copia local del valor a una variable local, pero pasa el objeto por referencia, es decir, apuntadores al objeto.
Admitiendo los dos puntos anteriores, expliquemos qué sucedió con el código de Python. Es solo por pasar por referencia para objetos, pero no tiene nada que ver con mutable / inmutable, o posiblemente el hecho de que la declaración "def" se ejecuta solo una vez cuando se define.
[] es un objeto, por lo que Python pasa la referencia de [] a
a
, es decir,a
es solo un puntero a [] que se encuentra en la memoria como un objeto. Sin embargo, solo hay una copia de [] con muchas referencias. Para el primer foo (), la lista [] se cambia a 1 por el método append. Pero tenga en cuenta que solo hay una copia del objeto de lista y este objeto ahora se convierte en 1 . Cuando se ejecuta el segundo foo (), lo que dice la página web de efbot (los elementos ya no se evalúan) está mal.a
se evalúa como el objeto de la lista, aunque ahora el contenido del objeto es 1 . ¡Este es el efecto de pasar por referencia! El resultado de foo (3) se puede derivar fácilmente de la misma manera.Para validar aún más mi respuesta, echemos un vistazo a dos códigos adicionales.
====== No. 2 ========
[]
es un objeto, también lo esNone
(el primero es mutable mientras que el segundo es inmutable. Pero la mutabilidad no tiene nada que ver con la pregunta). None está en algún lugar del espacio, pero sabemos que está allí y solo hay una copia de None allí. Por lo tanto, cada vez que se invoca foo, los elementos se evalúan (en oposición a alguna respuesta que solo se evalúa una vez) para ser Ninguno, para ser claros, la referencia (o la dirección) de Ninguno. Luego, en el foo, el elemento se cambia a [], es decir, apunta a otro objeto que tiene una dirección diferente.====== No. 3 =======
La invocación de foo (1) hace que los elementos apunten a un objeto de lista [] con una dirección, por ejemplo, 11111111. el contenido de la lista se cambia a 1 en la función foo en la secuela, pero la dirección no se cambia, todavía 11111111 Entonces foo (2, []) viene. Aunque [] en foo (2, []) tiene el mismo contenido que el parámetro predeterminado [] cuando llama a foo (1), ¡su dirección es diferente! Como proporcionamos el parámetro explícitamente,
items
debe tomar la dirección de este nuevo[]
, digamos 2222222, y devolverlo después de realizar algún cambio. Ahora se ejecuta foo (3). ya que solox
se proporciona, los elementos deben volver a tomar su valor predeterminado. ¿Cuál es el valor predeterminado? Se establece al definir la función foo: el objeto de lista ubicado en 11111111. Por lo tanto, los elementos se evalúan como la dirección 11111111 que tiene un elemento 1. La lista ubicada en 2222222 también contiene un elemento 2, pero no se señala con ningún elemento más. En consecuencia, un anexo de 3 haráitems
[1,3].De las explicaciones anteriores, podemos ver que la página web de efbot recomendada en la respuesta aceptada no dio una respuesta relevante a esta pregunta. Además, creo que un punto en la página web de efbot está mal. Creo que el código con respecto a la interfaz de usuario. El botón es correcto:
Cada botón puede contener una función de devolución de llamada distinta que mostrará un valor diferente de
i
. Puedo proporcionar un ejemplo para mostrar esto:Si ejecutamos
x[7]()
, obtendremos 7 como se esperaba yx[9]()
daremos 9, otro valor dei
.fuente
x[7]()
es así9
.TLDR: los valores predeterminados de tiempo de definición son consistentes y estrictamente más expresivos.
La definición de una función afecta a dos ámbitos: el ámbito de definición que contiene la función y el ámbito de ejecución contenido por la función. Si bien está bastante claro cómo se asignan los bloques a los ámbitos, la pregunta es a dónde
def <name>(<args=defaults>):
pertenece:La
def name
parte debe evaluarse en el ámbito de definición: queremosname
estar disponibles allí, después de todo. Evaluar la función solo dentro de sí misma la haría inaccesible.Como
parameter
es un nombre constante, podemos "evaluarlo" al mismo tiempo quedef name
. Esto también tiene la ventaja de que produce la función con una firma conocida comoname(parameter=...):
, en lugar de una simplename(...):
.Ahora, cuando evaluar
default
?La consistencia ya dice "en la definición": todo lo demás también
def <name>(<args=defaults>):
se evalúa mejor en la definición. Retrasar partes de ella sería la elección asombrosa.Las dos opciones tampoco son equivalentes: si
default
se evalúa en el momento de la definición, aún puede afectar el tiempo de ejecución. Sidefault
se evalúa en tiempo de ejecución, no puede afectar el tiempo de definición. Elegir "en la definición" permite expresar ambos casos, mientras que elegir "en la ejecución" puede expresar solo uno:fuente
def <name>(<args=defaults>):
se evalúa mejor en la definición". No creo que la conclusión se desprenda de la premisa. El hecho de que dos cosas estén en la misma línea no significa que deben evaluarse en el mismo alcance.default
es una cosa diferente al resto de la línea: es una expresión. Evaluar una expresión es un proceso muy diferente de definir una función.def
) o expresión (lambda
) no cambia que crear una función significa evaluación, especialmente de su firma. Y los valores predeterminados son parte de la firma de una función. Eso no por defecto medias tienen que ser evaluados de inmediato - Tipo consejos no puede, por ejemplo. Pero ciertamente sugiere que deberían hacerlo a menos que haya una buena razón para no hacerlo.Cualquier otra respuesta explica por qué este es realmente un comportamiento agradable y deseado, o por qué no debería necesitarlo de todos modos. El mío es para aquellos obstinados que quieren ejercer su derecho a doblegar el lenguaje a su voluntad, y no al revés.
"Arreglaremos" este comportamiento con un decorador que copiará el valor predeterminado en lugar de reutilizar la misma instancia para cada argumento posicional dejado en su valor predeterminado.
Ahora redefinimos nuestra función usando este decorador:
Esto es particularmente bueno para las funciones que toman múltiples argumentos. Comparar:
con
Es importante tener en cuenta que la solución anterior se rompe si intenta utilizar argumentos de palabras clave, de esta manera:
El decorador podría ajustarse para permitir eso, pero lo dejamos como un ejercicio para el lector;)
fuente