"Menos asombro" y el argumento por defecto mutable

2595

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 xestá efectivamente vinculado al valor predeterminado especificado cuando se llama a la función, no está definida, algo que presentaría un defecto profundo: la deflí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.

Stefano Borini
fuente
43
Pregunta complementaria - Buenos usos para argumentos por defecto mutables
Jonathan
44
No tengo dudas de que los argumentos mutables violan el principio de menos asombro para una persona promedio, y he visto a principiantes pisar allí, y luego reemplazar heroicamente las listas de correo con tuplas de correo. Sin embargo, los argumentos mutables todavía están en línea con Python Zen (Pep 20) y cae en la cláusula "obvio para holandés" (entendido / explotado por los programadores de python de núcleo duro). La solución recomendada con la cadena de documentos es la mejor, pero la resistencia a las cadenas de documentos y cualquier documento (escrito) no es tan infrecuente en la actualidad. Personalmente, preferiría un decorador (por ejemplo, @fixed_defaults).
Serge
55
Mi argumento cuando me encuentro con esto es: "¿Por qué necesita crear una función que devuelva un mutable que opcionalmente podría ser un mutable que pasaría a la función? ¿Altera un mutable o crea uno nuevo? ¿Por qué necesita hacer ambas cosas con una función? ¿Y por qué debería reescribirse el intérprete para permitirle hacer eso sin agregar tres líneas a su código? " Porque estamos hablando de reescribir la forma en que el intérprete maneja las definiciones de funciones y las evocaciones aquí. Eso es mucho por hacer para un caso de uso apenas necesario.
Alan Leuthard
12
"Los principiantes de Python esperarían que esta función siempre devuelva una lista con un solo elemento: [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].
symplectomorphic
2
Esta pregunta es "¿Por qué se implementó esto [de manera incorrecta]?" No pregunta "¿Cuál es la forma correcta?" , que está cubierto por [ ¿Por qué el uso de arg = None soluciona el problema de argumento predeterminado mutable de Python? ] * ( stackoverflow.com/questions/10676729/… ). Los nuevos usuarios casi siempre están menos interesados ​​en el primero y mucho más en el segundo, por lo que a veces es un enlace / engaño muy útil para citar.
smci

Respuestas:

1613

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.

robar
fuente
80
Para cualquiera que lea la respuesta anterior, le recomiendo que se tome el tiempo de leer el artículo vinculado de Effbot. ¡Además de toda la otra información útil, la parte sobre cómo se puede usar esta función de lenguaje para el almacenamiento en caché / memorización de resultados es muy útil!
Cam Jackson
85
Incluso si se trata de un objeto de primera clase, uno podría imaginar un diseño donde el código para cada valor predeterminado se almacena junto con el objeto y se reevalúa cada vez que se llama a la función. No digo que sería mejor, solo que las funciones como objetos de primera clase no lo impiden por completo.
gerrit
312
Lo sentimos, pero cualquier cosa considerada "El mayor WTF en Python" es definitivamente un defecto de diseño . Esta es una fuente de errores para todos en algún momento, porque nadie espera ese comportamiento al principio, lo que significa que, para empezar, no debería haber sido diseñado de esa manera. No me importa por qué aros tuvieron que saltar, deberían haber diseñado Python para que los argumentos predeterminados no sean estáticos.
BlueRaja - Danny Pflughoeft
192
Ya sea que se trate de una falla de diseño o no, su respuesta parece implicar que este comportamiento es de alguna manera necesario, natural y obvio dado que las funciones son objetos de primera clase, y ese simplemente no es el caso. Python tiene cierres. Si reemplaza el argumento predeterminado con una asignación en la primera línea de la función, evalúa la expresión de cada llamada (potencialmente utilizando nombres declarados en un ámbito de inclusión). No hay ninguna razón para que no sea posible o razonable evaluar los argumentos predeterminados cada vez que se llama a la función exactamente de la misma manera.
Mark Amery
24
El diseño no se sigue directamente de functions are objects. En su paradigma, la propuesta sería implementar los valores predeterminados de las funciones como propiedades en lugar de atributos.
bukzor
273

Supongamos que tiene el siguiente código

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

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

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

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 foofunció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:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Ahora, ¿utiliza mi mapa el valor de la StringBufferclave 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 objeto Mapusando 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.

Eli Courtwright
fuente
30
Creo que es una cuestión de debate. Estás actuando sobre una variable global. Cualquier evaluación realizada en cualquier parte de su código que involucre su variable global ahora (correctamente) se referirá a ("arándanos", "mangos"). El parámetro predeterminado podría ser como cualquier otro caso.
Stefano Borini el
47
En realidad, no creo estar de acuerdo con tu primer ejemplo. No estoy seguro de que me guste la idea de modificar un inicializador como ese en primer lugar, pero si lo hiciera, esperaría que se comportara exactamente como usted describe, cambiando el valor predeterminado a ("blueberries", "mangos").
Ben Blank
12
El parámetro predeterminado es como cualquier otro caso. Lo inesperado es que el parámetro es una variable global y no local. Lo que a su vez se debe a que el código se ejecuta en la definición de la función, no en la llamada. Una vez que obtienes eso, y que lo mismo ocurre con las clases, queda perfectamente claro.
Lennart Regebro
17
El ejemplo me parece engañoso en lugar de brillante. Sisome_random_function() APPENDs al fruitslugar de asignar a la misma, el comportamiento de eat() 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.
alexis
13
Acabas de declarar globaly reasignar explícitamente la tupla: no hay absolutamente nada sorprendente si eatfunciona de manera diferente después de eso.
user3467349
241

La parte relevante de la documentación :

Los valores de los parámetros predeterminados se evalúan de izquierda a derecha 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. Esto es especialmente importante de entender cuando un parámetro predeterminado es un objeto mutable, como una lista o un diccionario: si la función modifica el objeto (por ejemplo, al agregar un elemento a una lista), el valor predeterminado se modifica en efecto. Esto generalmente no es lo que se pretendía. Una forma de evitar esto es usarlo Nonepor defecto y probarlo explícitamente en el cuerpo de la función, por ejemplo:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin
glglgl
fuente
180
Las frases "generalmente no es lo que se pretendía" y "una forma de evitar esto es" huelen como si estuvieran documentando una falla de diseño.
bukzor
44
@Matthew: Soy muy consciente, pero no vale la pena. Por lo general, verá que las guías de estilo y los linters marcan incondicionalmente los valores predeterminados mutables como incorrectos por este motivo. La forma explícita de hacer lo mismo es rellenar un atributo en la función ( function.data = []) o, mejor aún, hacer un objeto.
bukzor
66
@bukzor: Es necesario tener en cuenta y documentar las trampas, por eso esta pregunta es buena y ha recibido tantos votos positivos. Al mismo tiempo, las trampas no necesariamente deben eliminarse. ¿Cuántos principiantes de Python han pasado una lista a una función que la modificó y se sorprendieron al ver los cambios mostrados en la variable original? Sin embargo, los tipos de objetos mutables son maravillosos cuando entiendes cómo usarlos. Supongo que todo se reduce a una opinión sobre este escollo en particular.
Matthew
33
La frase "esto no es generalmente lo que se pretendía" significa "no lo que el programador realmente quería que sucediera", no "no lo que se supone que debe hacer Python".
holdenweb
44
@holdenweb Wow, llego mega tarde a la fiesta. Dado el contexto, bukzor tiene toda la razón: están documentando comportamiento / consecuencia que no fue "intencionado" cuando decidieron que el lenguaje debería ejecutar la definición de la función. Dado que es una consecuencia no deseada de su elección de diseño, es un defecto de diseño. Si no se tratara de una falla de diseño, no habría necesidad de ofrecer "una forma de evitar esto".
code_dredd
118

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:

a = []

espera obtener una nueva lista referenciada por a.

¿Por qué debería el a=[]en

def x(a=[]):

instanciar 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:

def x(a=datetime.datetime.now()):

usuario, ¿desea apredeterminar la fecha y hora correspondiente a cuando está definiendo o ejecutando x? 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:

b = datetime.datetime.now()
def x(a=b):

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:

def x(static a=b):
Utaal
fuente
11
Puede hacer: def x (a = Ninguno): y luego, si a es Ninguno, establezca a = datetime.datetime.now ()
Anon
20
Gracias por esto. Realmente no pude entender por qué esto me irrita sin fin. Lo has hecho maravillosamente con un mínimo de confusión y confusión. Como alguien que viene de la programación de sistemas en C ++ y, a veces, ingenuamente "traduce" características del lenguaje, este falso amigo me dio una patada en la cabeza a lo grande, al igual que los atributos de clase. Entiendo por qué las cosas son así, pero no puedo evitar que no me guste, sin importar lo positivo que pueda resultar. Por lo menos es tan contrario a mi experiencia, que probablemente voy (con suerte) nunca olvido que ...
AndreasT
55
@Andreas, una vez que usa Python durante el tiempo suficiente, comienza a ver cuán lógico es para Python interpretar las cosas como atributos de clase de la manera en que lo hace, es solo debido a las peculiaridades y limitaciones particulares de lenguajes como C ++ (y Java, y C # ...) que tiene sentido que los contenidos del 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).
Karl Knechtel
66
La estructura normativa no es ninguna peculiaridad o limitación en mi libro. Sé que puede ser torpe y feo, pero puedes llamarlo una "definición" de algo. Los lenguajes dinámicos me parecen un poco anarquistas: claro, todos son libres, pero se necesita estructura para que alguien vacíe la basura y allane el camino. Supongo que soy viejo ... :)
AndreasT
44
La definición de la función se ejecuta en el tiempo de carga del módulo. El cuerpo de la función se ejecuta en el momento de la llamada a la función. El argumento predeterminado es parte de la definición de la función, no del cuerpo de la función. (Se vuelve más complicado para las funciones anidadas)
Lutz Prechelt
84

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:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

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.

Lennart Regebro
fuente
¿Cómo define un atributo de clase que es diferente para cada instancia de una clase?
Kieveli el
19
Si es diferente para cada instancia, no es un atributo de clase. Los atributos de clase son atributos en la CLASE. De ahí el nombre. Por lo tanto, son iguales para todas las instancias.
Lennart Regebro
1
¿Cómo define un atributo en una clase que es diferente para cada instancia de una clase? (Redefinido para aquellos que no pudieron determinar que una persona que no esté familiarizada con las convenciones de nomenclatura de Python podría estar preguntando acerca de las variables miembro normales de una clase).
Kieveli el
@Kievieli: ESTÁS hablando de variables miembro normales de una clase. :-) Defina los atributos de instancia diciendo self.attribute = value en cualquier método. Por ejemplo __init __ ().
Lennart Regebro
@Kieveli: Dos respuestas: no puede, porque cualquier cosa que defina a nivel de clase será un atributo de clase, y cualquier instancia que acceda a ese atributo accederá al mismo atributo de clase; puede, / tipo de /, usando propertys, 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 (usando self.attribute = valuecomo dijo Lennart).
Ethan Furman
66

¿Por qué no introspectivas?

Estoy realmente sorprendido de que nadie haya realizado la perspicaz introspección ofrecida por Python ( 2y 3aplicar) en las llamadas.

Dada una pequeña función simple funcdefinida como:

>>> def func(a = []):
...    a.append(5)

Cuando Python lo encuentra, lo primero que hará es compilarlo para crear un codeobjeto 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 lista aahora puede considerarse un miembro de la función func.

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.xpara esto, para Python 2 lo mismo se aplica (uso __defaults__o func_defaultsen Python 2; sí, dos nombres para la misma cosa).

Función antes de la ejecución:

>>> def func(a = []):
...     a.append(5)
...     

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):

>>> func.__defaults__
([],)

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:

>>> func()

Ahora, veamos esos de __defaults__nuevo:

>>> func.__defaults__
([5],)

¿Asombrado? ¡El valor dentro del objeto cambia! Las llamadas consecutivas a la función ahora simplemente se agregarán a ese listobjeto incrustado :

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

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 Nonecomo predeterminado y luego inicializar en el cuerpo de la función:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

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ón func, simplemente puede cambiar su función para devolver idla lista autilizada 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:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

¡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:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

como notará, input()se llama antes del proceso de construcción de la función y se vincula al nombre bar.

Dimitris Fasarakis Hilliard
fuente
1
¿Es id(...)necesario para esa última verificación, o el isoperador respondería la misma pregunta?
das-g
1
@ das-g isestaría bien, solo lo usé id(val)porque creo que podría ser más intuitivo.
Dimitris Fasarakis Hilliard
El uso Nonecomo 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.
Brilliand
58

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

def foo(arg=something_expensive_to_compute())):
    ...

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:

funcs = [ lambda i=i: i for i in range(10)]

Esto devuelve una lista de funciones que devuelven 0,1,2,3 ... respectivamente. Si se cambia el comportamiento, en su lugar se unirán ial valor de tiempo de llamada de i, por lo que obtendría una lista de funciones que todos devuelven 9.

La única forma de implementar esto de otra manera sería crear un cierre adicional con el límite i, es decir:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Introspección

Considera el código:

def foo(a='test', b=100, c=[]):
   print a,b,c

Podemos obtener información sobre los argumentos y valores predeterminados utilizando el inspectmódulo, que

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Esta 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:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

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.

Brian
fuente
1
también podría lograr la introspección si para cada uno hubiera una función para crear el argumento predeterminado en lugar de un valor. el módulo de inspección solo llamará a esa función.
yairchu
@SilentGhost: estoy hablando de si el comportamiento se cambió para recrearlo; crearlo una vez es el comportamiento actual y por qué existe el problema predeterminado mutable.
Brian
1
@yairchu: Eso supone que la construcción es segura (es decir, no tiene efectos secundarios). La introspección de los argumentos no debería hacer nada, pero evaluar un código arbitrario podría terminar teniendo un efecto.
Brian
1
Un diseño de lenguaje diferente a menudo solo significa escribir cosas de manera diferente. Su primer ejemplo podría escribirse fácilmente como: _expensive = caro (); def foo (arg = _expensive), si específicamente no desea que se reevalúe.
Glenn Maynard el
@Glenn, a eso me refería con "almacenar en caché la variable externamente", es un poco más detallado y, sin embargo, terminas con variables adicionales en tu espacio de nombres.
Brian
55

5 puntos en defensa de Python

  1. Simplicidad : el comportamiento es simple en el siguiente sentido: la mayoría de las personas caen en esta trampa solo una vez, no varias.

  2. 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.

  3. 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.)

  4. 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.

  5. 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.

Lutz Prechelt
fuente
18
Me tomó un año descubrir que este comportamiento está desordenando mi código en la producción, terminé eliminando una característica completa hasta que me topé con este defecto de diseño por casualidad. Estoy usando Django. Dado que el entorno de preparación no tenía muchas solicitudes, este error nunca tuvo ningún impacto en el control de calidad. Cuando nos pusimos en marcha y recibimos muchas solicitudes simultáneas, algunas funciones de utilidad comenzaron a sobrescribir los parámetros de cada uno. Hacer agujeros de seguridad, errores y lo que no.
oriadam
77
@oriadam, no te ofendas, pero me pregunto cómo aprendiste Python sin toparte con esto antes. Ahora estoy aprendiendo Python y esta posible trampa se menciona en el tutorial oficial de Python junto con la primera mención de los argumentos predeterminados. (Como se menciona en el punto 4 de esta respuesta). Supongo que la moraleja es, más bien sin simpatía, leer los documentos oficiales del lenguaje que usa para crear software de producción.
Comodín
Además, sería sorprendente (para mí) si se llamara a una función de complejidad desconocida además de la llamada a la función que estoy haciendo.
Vatine
52

Este comportamiento se explica fácilmente por:

  1. La declaración de función (clase, etc.) se ejecuta solo una vez, creando todos los objetos de valor predeterminado
  2. todo pasa por referencia

Entonces:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a no cambia - cada llamada de asignación crea un nuevo objeto int - se imprime un nuevo objeto
  2. b no cambia: la nueva matriz se crea a partir del valor predeterminado y se imprime
  3. c cambios - la operación se realiza en el mismo objeto - y se imprime
ymv
fuente
(En realidad, agregar es un mal ejemplo, pero los enteros siendo inmutables todavía es mi punto principal.)
Anon
Me disgusté después de comprobar que, con b establecido en [], b .__ add __ ([1]) devuelve [1] pero también deja b todavía [] aunque las listas son mutables. Culpa mía.
Anon
@ANon: lo hay __iadd__, pero no funciona con int. Por supuesto. :-)
Veky
35

Lo que estás preguntando es por qué esto:

def func(a=[], b = 2):
    pass

no es internamente equivalente a esto:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

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.

Glenn Maynard
fuente
66
No necesitaría ser un cierre, una mejor manera de pensarlo sería simplemente hacer que el bytecode creando valores predeterminados sea la primera línea de código; después de todo, está compilando el cuerpo en ese punto de todos modos, no hay una diferencia real entre el código en los argumentos y el código en el cuerpo.
Brian
10
Es cierto, pero aún así ralentizaría Python, y en realidad sería bastante sorprendente, a menos que haga lo mismo para las definiciones de clase, lo que lo haría estúpidamente lento ya que tendría que volver a ejecutar toda la definición de clase cada vez que crea una instancia clase. Como se mencionó, la solución sería más sorprendente que el problema.
Lennart Regebro
De acuerdo con Lennart. Como a Guido le gusta decir, por cada característica de idioma o biblioteca estándar, hay alguien por ahí que la usa.
Jason Baker
66
Cambiarlo ahora sería una locura: solo estamos explorando por qué es así. Si, para empezar, hiciera una evaluación predeterminada tardía, no sería necesariamente sorprendente. Definitivamente es cierto que una diferencia de análisis tan importante tendría efectos radicales, y probablemente muchos oscuros, en el lenguaje en su conjunto.
Glenn Maynard
35

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:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Solución : una copia
una solución absolutamente seguro es que copyni deepcopyel objeto de entrada primero y luego hacer lo que sea con la copia.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Muchos tipos mutables incorporados tienen un método de copia como some_dict.copy()osome_set.copy() , o se pueden copiar fácilmente como somelist[:]o list(some_list). Cada objeto también puede ser copiado copy.copy(any_object)o más completo por copy.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 copiar

Problema de ejemplo para una pregunta SO similar

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

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, _var1es 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 esdef ...(var1=None): if var1 is None: var1 = [] Más ...

3) En algunos casos es útil el comportamiento mutable de los parámetros predeterminados .

hynekcer
fuente
55
Espero que sepas que Python no es un lenguaje de programación funcional.
Veky
66
Sí, Python es un lenguaje multi-paragigm con algunas características funcionales. ("No haga que cada problema parezca un clavo solo porque tiene un martillo".) Muchos de ellos se encuentran en las mejores prácticas de Python. Python tiene un COMO interesante Programación funcional Otras características son los cierres y el curry, no mencionados aquí.
hynekcer
1
También agregaría, en esta etapa tardía, que la semántica de asignación de Python se ha diseñado explícitamente para evitar la copia de datos cuando sea necesario, por lo que la creación de copias (y especialmente de copias profundas) afectará negativamente tanto el tiempo de ejecución como el uso de memoria. Por lo tanto, deben usarse solo cuando sea necesario, pero los recién llegados a menudo tienen dificultades para comprender cuándo es eso.
holdenweb
1
@holdenweb Estoy de acuerdo. Una copia temporal es la forma más habitual y, a veces, la única forma posible de proteger los datos mutables originales de una función extraña que los modifica potencialmente. Afortunadamente, una función que modifica irrazonablemente los datos se considera un error y, por lo tanto, es poco común.
hynekcer
Estoy de acuerdo con esta respuesta Y no entiendo por qué 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 haces if a is None: a = [1, 2, 3], copias la lista de todos modos.
koddo
30

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.

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

No hay valores predeterminados a la vista en este código, pero obtiene exactamente el mismo problema.

El problema es que fooestá 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 modificar asi 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.

Ben
fuente
77
Aunque relacionado, creo que este es un comportamiento distinto (ya que esperamos appendcambiar a"en el lugar"). Que un mutable predeterminado no se vuelva a instanciar en cada llamada es el bit "inesperado" ... al menos para mí. :)
Andy Hayden
2
@AndyHayden si se espera que la función modifique el argumento, ¿por qué tendría sentido tener un valor predeterminado?
Mark Ransom
@MarkRansom el único ejemplo que puedo pensar es 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.
Andy Hayden
1
@AndyHayden Dejé mi propia respuesta aquí con una expansión de ese sentimiento. Déjame saber lo que piensas. Podría agregar su ejemplo de cache={}para completar.
Mark Ransom
1
@AndyHayden El punto de mi respuesta es que si alguna vez te asombra mutar accidentalmente el valor predeterminado de un argumento, entonces tienes otro error, que es que tu código puede mutar accidentalmente el valor de una persona que llama cuando no se usó el valor predeterminado . Y tenga en cuenta que el uso Noney la asignación del valor predeterminado real si el argumento arg None 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".
Ben
27

Tema ya ocupado, pero por lo que leí aquí, lo siguiente me ayudó a darme cuenta de cómo funciona internamente:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232
Stéphane
fuente
2
En realidad, esto puede ser un poco confuso para los recién llegados como a = a + [1]sobrecargas a... considere cambiarlo b = a + [1] ; print id(b)y agregar una línea a.append(2). Eso hará que sea más obvio que +en dos listas siempre se crea una nueva lista (asignada a b), mientras que una modificación aaún puede tener la misma id(a).
Jörn Hees
25

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?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Te daré una pista. Aquí está el desmontaje (ver http://docs.python.org/library/dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Dudo que el comportamiento experimentado tenga un uso práctico (¿quién realmente usó variables estáticas en C, sin criar insectos?)

Como 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.

Jason Baker
fuente
24

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:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

es casi exactamente equivalente a esto:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Demostración

Aquí hay una demostración: puede verificar que son el mismo objeto cada vez que los hace referencia

  • viendo que la lista se crea antes de que la función haya terminado de compilarse en un objeto de función,
  • observando que la identificación es la misma cada vez que se hace referencia a la lista,
  • observando que la lista permanece cambiada cuando la función que la usa se llama por segunda vez,
  • observando el orden en que se imprime la salida desde la fuente (que convenientemente numeré para usted):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

y ejecutarlo con python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

¿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:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

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 :

Si no desea que el valor predeterminado se comparta entre llamadas posteriores, puede escribir la función de esta manera:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
Aaron Hall
fuente
24

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:

def a(): return []

def b(x=a()):
    print x

Con suerte, es suficiente para mostrar que no ejecutar las expresiones de argumento predeterminadas en el momento de la ejecución de la defdeclaració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.

Baczek
fuente
20

Una solución simple usando None

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]
hugo24
fuente
19

Este comportamiento no es sorprendente si tiene en cuenta lo siguiente:

  1. El comportamiento de los atributos de clase de solo lectura en los intentos de asignación, y que
  2. Las funciones son objetos (explicados bien en la respuesta aceptada).

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:

... todas las variables que se encuentran fuera del ámbito más interno son de solo lectura ( un intento de escribir en dicha variable simplemente creará una nueva variable local en el ámbito más interno, dejando la variable externa con el mismo nombre sin cambios ).

Vuelva al ejemplo original y considere los puntos anteriores:

def foo(a=[]):
    a.append(5)
    return a

Aquí foohay un objeto y aes un atributo de foo(disponible en foo.func_defs[0]). Dado que aes una lista, aes mutable y, por lo tanto, es un atributo de lectura-escritura de foo. 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 foosin anular un valor predeterminado utiliza el valor de ese valor predeterminado foo.func_defs. En este caso, foo.func_defs[0]se utiliza para ael alcance del código del objeto de función. Cambios a acambio foo.func_defs[0], que es parte del fooobjeto y persiste entre la ejecución del código en foo.

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:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Teniendo en cuenta (1) y (2) , uno puede ver por qué esto logra el comportamiento deseado:

  • Cuando se crea una fooinstancia del objeto de función, foo.func_defs[0]se establece en Noneun objeto inmutable.
  • Cuando la función se ejecuta con los valores predeterminados (sin ningún parámetro especificado Len la llamada a la función), foo.func_defs[0]( None) está disponible en el ámbito local como L.
  • Tras L = [], la asignación no puede tener éxito en foo.func_defs[0], porque ese atributo es de solo lectura.
  • Por (1) , una nueva variable local también nombrada Lse 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 de foo.
Dmitry Minkovsky
fuente
19

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 ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Puede verificar que son el mismo objeto usando id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Según "Python eficaz: 59 formas específicas de escribir mejor Python" de Brett Slatkin, Elemento 20: Uso Noney cadenas de documentos para especificar argumentos dinámicos predeterminados (p. 48)

La convención para lograr el resultado deseado en Python es proporcionar un valor predeterminado Noney documentar el comportamiento real en la cadena de documentación.

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 :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

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.

Alejandro
fuente
17

Las soluciones aquí son:

  1. Úselo Nonecomo su valor predeterminado (o un nonce object) y actívelo para crear sus valores en tiempo de ejecución; o
  2. Use a lambdacomo 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)

Marcin
fuente
16

Cuando hacemos esto:

def foo(a=[]):
    ...

... asignamos el argumento aa 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?

def foo(a=pavlo):
   ...

En cualquier momento, si la persona que llama no nos dice qué aes, lo reutilizamos pavlo.

Si pavloes mutable (modificable) y footermina modificándolo, un efecto que notamos la próxima vez foose llama sin especificar a.

Entonces esto es lo que ves (recuerda, pavlose inicializa a []):

 >>> foo()
 [5]

Ahora, pavloes [5].

Llamar foo()nuevamente modifica pavlonuevamente:

>>> foo()
[5, 5]

Especificar acuándo llamar foo()asegura pavloque no se toque.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Entonces, pavlotodavía está [5, 5].

>>> foo()
[5, 5, 5]
Saish
fuente
16

A veces exploto este comportamiento como una alternativa al siguiente patrón:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Si singletonsolo lo usa use_singleton, me gusta el siguiente patrón como reemplazo:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

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.

bgreen-litl
fuente
2
Prefiero agregar un decorador para la memorización y colocar el caché de la memoria en el objeto de la función.
Stefano Borini
Este ejemplo no reemplaza el patrón más complejo que muestra, porque llama _make_singletonen 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.
Yann Vernier el
15

Puede evitar esto reemplazando el objeto (y, por lo tanto, el vínculo con el alcance):

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

Feo, pero funciona.

joedborg
fuente
3
Esta es una buena solución en los casos en que está utilizando un software de generación automática de documentación para documentar los tipos de argumentos esperados por la función. Poner a = None y luego configurar a en [] si a es None no ayuda al lector a comprender de un vistazo lo que se espera.
Michael Scott Cuthbert
Buena idea: volver a unir ese nombre garantiza que nunca se pueda modificar. Realmente me gusta eso.
holdenweb
Esta es exactamente la forma de hacerlo. Python no hace una copia del parámetro, por lo que depende de usted hacer la copia explícitamente. Una vez que tenga una copia, puede modificarla a su gusto sin efectos secundarios inesperados.
Mark Ransom
13

Puede ser cierto que:

  1. Alguien está utilizando todas las funciones de idioma / biblioteca, y
  2. Cambiar el comportamiento aquí sería desaconsejado, pero

es completamente consistente mantener ambas características anteriores y aún hacer otro punto:

  1. Es una característica confusa y desafortunada en Python.

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 .

Christos Hayward
fuente
66
-1 Aunque es una perspectiva defendible, esta no es una respuesta, y no estoy de acuerdo con ella. Demasiadas excepciones especiales engendran sus propios casos de esquina.
Marcin
3
Entonces, es "increíblemente ignorante" decir que en Python tendría más sentido que un argumento predeterminado de [] permanezca [] cada vez que se llama a la función.
Christos Hayward
3
Y es ignorante considerar como un idioma desafortunado establecer un argumento predeterminado en Ninguno, y luego en el cuerpo del cuerpo de la función si argumento == Ninguno: argumento = []? ¿Es ignorante considerar este idioma desafortunado ya que a menudo la gente quiere lo que un recién llegado ingenuo esperaría, que si asigna f (argumento = []), el argumento automáticamente cambiará a un valor de []?
Christos Hayward
3
Pero en Python, parte del espíritu del lenguaje es que no tienes que tomar demasiadas inmersiones profundas; array.sort () funciona, y funciona independientemente de lo poco que entienda sobre ordenación, big-O y constantes. La belleza de Python en el mecanismo de clasificación de matriz, para dar uno de los innumerables ejemplos, es que no es necesario que te sumerjas en lo interno. Y para decirlo de otra manera, la belleza de Python es que normalmente no se requiere profundizar en la implementación para obtener algo que simplemente funciona. Y hay una solución alternativa (... si argumento == Ninguno: argumento = []), FALLO.
Christos Hayward
3
Como una declaración independiente, la declaración x=[]significa "crear un objeto de lista vacío y vincular el nombre 'x' a él". Entonces, en def 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?
Jerry B
10

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:

  1. Tiene la intención de modificar el argumento como un efecto secundario de la función. En este caso, nunca tiene sentido tener un argumento predeterminado. La única excepción es cuando abusa de la lista de argumentos para tener atributos de función, por ejemplo cache={}, y no se espera que llame a la función con un argumento real.
  2. Tiene la intención de dejar el argumento sin modificar, pero accidentalmente lo modificó. Eso es un error, arréglalo.
  3. Tiene la intención de modificar el argumento para su uso dentro de la función, pero no esperaba que la modificación fuera visible fuera de la función. En ese caso, debe hacer una copia del argumento, ¡ya sea el predeterminado o no! Python no es un lenguaje de llamada por valor, por lo que no hace la copia por usted, debe ser explícito al respecto.

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.

Mark Ransom
fuente
"Hacer algo mal" es el diagnóstico. Dicho esto, creo que hay momentos en que el patrón = Ninguno es útil, pero generalmente no desea modificar si se pasa un mutable en ese caso (2). ¡El cache={}patrón es realmente una solución de solo entrevista, en el código real que probablemente quieras @lru_cache!
Andy Hayden
9

¡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.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

imprime lo siguiente

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way
Norfeldt
fuente
8

Simplemente cambie la función para que sea:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
Ytpillai
fuente
7

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, aes 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. ase 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 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]es un objeto, también lo es None(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 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,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, itemsdebe 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 soloxse 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:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

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:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Si ejecutamos x[7](), obtendremos 7 como se esperaba y x[9]()daremos 9, otro valor de i.

usuario2384994
fuente
55
Tu último punto está mal. Pruébalo y verás que x[7]()es así 9.
Duncan
2
"python pasa el tipo de datos elementales por valor, es decir, hacer una copia local del valor en una variable local" es completamente incorrecto. Estoy asombrado de que alguien obviamente pueda conocer Python muy bien, sin embargo, tenga una mala comprensión de los fundamentos. :-(
Veky
6

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:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

La def nameparte debe evaluarse en el ámbito de definición: queremos nameestar disponibles allí, después de todo. Evaluar la función solo dentro de sí misma la haría inaccesible.

Como parameteres un nombre constante, podemos "evaluarlo" al mismo tiempo que def name. Esto también tiene la ventaja de que produce la función con una firma conocida como name(parameter=...):, en lugar de una simple name(...):.

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 defaultse evalúa en el momento de la definición, aún puede afectar el tiempo de ejecución. Si defaultse 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:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...
MisterMiyagi
fuente
"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". 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. defaultes 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.
LarsH
@LarsH Las definiciones de funciones se evalúan en Python. Si eso es de una declaració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.
MisterMiyagi
OK, crear una función significa evaluación en algún sentido, pero obviamente no en el sentido de que cada expresión dentro de ella se evalúa en el momento de la definición. La mayoría no lo son. No me queda claro en qué sentido la firma se "evalúa" especialmente en el momento de la definición más de lo que se "evalúa" el cuerpo de la función (analizado en una representación adecuada); mientras que las expresiones en el cuerpo de la función claramente no se evalúan en el sentido completo. Desde este punto de vista, la coherencia diría que las expresiones en la firma tampoco deberían evaluarse "completamente".
LarsH
No quiero decir que estés equivocado, solo que tu conclusión no se sigue solo de la consistencia.
LarsH
Los valores predeterminados de @LarsH no son parte del cuerpo, ni estoy afirmando que la consistencia es el único criterio. ¿Puedes hacer una sugerencia sobre cómo aclarar la respuesta?
MisterMiyagi
3

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.

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Ahora redefinimos nuestra función usando este decorador:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired

Esto es particularmente bueno para las funciones que toman múltiples argumentos. Comparar:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

con

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Es importante tener en cuenta que la solución anterior se rompe si intenta utilizar argumentos de palabras clave, de esta manera:

foo(a=[4])

El decorador podría ajustarse para permitir eso, pero lo dejamos como un ejercicio para el lector;)

Przemek D
fuente