El "menor asombro" y el argumento predeterminado mutable

2943

Cualquiera que esté jugando con Python el tiempo suficiente ha sido mordido (o hecho pedazos) 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]. En cambio, el resultado es 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 la llamó "un defecto de diseño dramático" del lenguaje. Le respondí que el comportamiento tenía una explicación subyacente, y de hecho es muy desconcertante e inesperado si no comprende los aspectos internos. 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 errores?)

Editar :

Baczek dio 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 colocar el alcance de los parámetros: ¿dentro de la función o "junto" con ella?

Hacer la vinculación dentro de la función significaría que xestá efectivamente vinculada al valor predeterminado especificado cuando se llama a la función, no definida, algo que presentaría un defecto profundo: la deflínea sería "híbrida" en el sentido de que parte de la vinculación (de el objeto de función) ocurriría en la definición, y parte (asignación de parámetros predeterminados) en el momento de la 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.

17
  • 60
    Pregunta complementaria - Buenos usos para argumentos predeterminados mutablesJonathan 6/02/12 a las 20:54
  • 7
    No tengo ninguna duda de que los argumentos cambiantes violan el principio del más mínimo asombro para una persona promedio, y he visto a principiantes entrar 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 entran en la cláusula "obvio para holandés" (entendido / explotado por programadores de Python de núcleo duro). La solución alternativa 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 hoy en día. Personalmente, preferiría un decorador (digamos @fixed_defaults). Serge 6/04/2017 a las 16:04
  • 5
    Mi argumento cuando me encuentro con esto es: "¿Por qué necesitas crear una función que devuelva un mutable que, opcionalmente, podría ser un mutable que pasarías a la función? O modifica un mutable o crea uno nuevo. ¿Por qué necesitas 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 1 de junio de 2017 a las 21:22
  • 21
    "Los principiantes de Python esperarían que esta función siempre devuelva una lista con un solo elemento: [5]". Soy un novato en Python, y no esperaría esto, porque obviamente foo([1])regresará [1, 5], no [5]. Lo que quería decir es que un novato esperaría que la función llamada sin parámetro siempre regresara [5]. symplectomorphic 6 de julio de 2017 a las 16:08
  • 5
    Esta pregunta pregunta: "¿Por qué se implementó esto [de la manera incorrecta]?" No pregunta "¿Cuál es la forma correcta?" , que está cubierto por [ ¿Por qué el uso de arg = None soluciona el problema del argumento predeterminado mutable de Python? ] * ( stackoverflow.com/questions/10676729/… ). Los nuevos usuarios casi siempre están menos interesados ​​en lo primero y mucho más en lo segundo, por lo que a veces es un enlace / engaño muy útil para citar. smci 21/04/19 a las 8:48
1763

En realidad, esto no es un defecto de diseño y no se debe a los componentes internos o al 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 lo piense de esta manera, entonces tiene todo el sentido: una función es un objeto que está siendo evaluado 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 Valores de parámetros predeterminados en Python .
Lo encontré muy claro, y realmente sugiero leerlo para conocer mejor cómo funcionan los objetos funcionales.

14
  • 88
    A cualquiera que lea la respuesta anterior, le recomiendo encarecidamente 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 utilizar esta función de idioma para el almacenamiento en caché / memorización de resultados es muy útil. Cam Jackson 14/10/11 a las 0:05
  • 110
    Incluso si es 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 estoy diciendo que eso sea mejor, solo que las funciones que son objetos de primera clase no lo excluyen por completo. gerrit 11 de enero de 2013 a las 10:55
  • 411
    Lo siento, pero cualquier cosa que se considere "La 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 no debería haber sido diseñado de esa manera para empezar. No me importa qué obstáculos tuvieron que atravesar, deberían haber diseñado Python para que los argumentos predeterminados no sean estáticos. BlueRaja - Danny Pflughoeft 7/0613 a las 21:28
  • 245
    Ya sea que se trate de un defecto 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 en cada llamada (potencialmente usando nombres declarados en un ámbito adjunto). 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 8 de enero de 2014 a las 22:16
  • 35
    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 3 de mayo de 2014 a las 20:46
295

Suponga que tiene el siguiente código

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

def eat(food=fruits):
    ...

Cuando veo la declaración de eat, lo menos sorprendente es pensar que si no se da el primer parámetro será igual a la tupla ("apples", "bananas", "loganberries")

Sin embargo, supongamos que 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 estuvieran vinculados a 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 se han cambiado las frutas. En mi opinión, esto sería más asombroso que descubrir que su foofunción anterior estaba mutando la lista.

El verdadero problema radica en las variables mutables, y todos los lenguajes 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, ¿mi mapa usa 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 intentó sacar el objeto Mapusando un valor idéntico al que lo puso, o la persona que parece que no puede recuperar su objeto a pesar de que la clave que está usando es literalmente el mismo objeto que se usó para ponerlo en el mapa (esta es realmente la razón por la que Python no permite que sus tipos de datos incorporados mutables se usen como claves de diccionario).

Su ejemplo es bueno para un caso en el que los recién llegados a Python serán sorprendidos y mordidos. Pero yo diría que si "arreglamos" esto, eso solo crearía una situación diferente en la que serían mordidos en su lugar, y esa sería aún menos intuitiva. Además, este es siempre el caso cuando se trata de variables mutables; siempre se encuentra con casos en los que alguien podría 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 la función está definida y ese objeto es siempre el predeterminado. Supongo que podrían usar un caso especial usando una lista vacía, pero ese tipo de carcasa especial causaría aún más asombro, sin mencionar que sería incompatible con versiones anteriores.

dieciséis
  • 43
    Creo que es un tema 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 se referirá (correctamente) a ("arándanos", "mangos"). el parámetro predeterminado podría ser como cualquier otro caso. Stefano Borini 15 de julio de 2009 a las 18:16
  • 62
    En realidad, no creo que esté de acuerdo con su 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 comporte exactamente como lo describe, cambiando el valor predeterminado a ("blueberries", "mangos"). Ben Blank 15 de julio de 2009 a las 18:26
  • 14
    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 lo entiendes, y lo mismo ocurre con las clases, queda perfectamente claro. Lennart Regebro 15 de julio de 2009 a las 18:59
  • 24
    Encuentro el ejemplo engañoso más que brillante. Si some_random_function()APPENDs al fruitslugar de asignar a la misma, el comportamiento de eat() van a cambiar. Hasta aquí el maravilloso diseño actual. Si usa un argumento predeterminado al que se hace referencia en otro lugar y luego modifica la referencia desde fuera de la función, está buscando problemas. La verdadera WTF es cuando las personas definen un nuevo argumento predeterminado (un literal de lista o una llamada a un constructor) y aún así obtienen un poco. alexis 9/10/2014 a las 15:37
  • 20
    Acaba de declarar globaly reasignar explícitamente la tupla; no hay absolutamente nada sorprendente si eatfunciona de manera diferente después de eso. user3467349 26/01/15 a las 16:07
263

La parte relevante de la documentación :

Default parameter values are evaluated from left to right when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call. This is especially important to understand when a default parameter is a mutable object, such as a list or a dictionary: if the function modifies the object (e.g. by appending an item to a list), the default value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function, e.g.:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin
8
  • 220
    Las frases "esto no es generalmente lo que se pretendía" y "una forma de evitarlo" huelen como si estuvieran documentando un defecto de diseño. bukzor 3 de mayo de 2014 a las 20:53
  • 11
    @bukzor: Es necesario anotar y documentar las trampas, razón por la cual esta pregunta es buena y ha recibido tantos votos a favor. Al mismo tiempo, no es necesario eliminar las trampas. ¿Cuántos principiantes de Python han pasado una lista a una función que la modificó y se sorprendieron al ver que los cambios aparecían en la variable original? Sin embargo, los tipos de objetos mutables son maravillosos cuando se sabe cómo utilizarlos. Supongo que se reduce a una opinión sobre este escollo en particular. Matthew 19/06/2014 a las 17:54
  • 36
    La frase "esto no es generalmente lo que se pretendía" significa "no lo que el programador realmente quería que sucediera", no "no es lo que se supone que debe hacer Python". holdenweb 19/12/14 a las 11:48
  • 9
    @holdenweb Vaya, llegué mega-tarde a la fiesta. Dado el contexto, bukzor tiene toda la razón: están documentando un comportamiento / consecuencia que no fue "previsto" cuando decidieron que el lenguaje debería ejecutar la definición de la función. Dado que es una consecuencia involuntaria de su elección de diseño, es un defecto de diseño. Si no fuera un defecto de diseño, no habría necesidad de ofrecer "una forma de evitar esto". code_dredd 3 oct 2017 a las 7:35
  • 6
    Podríamos llevarlo a charlar y discutir de qué otra manera podría ser, pero la semántica se ha debatido a fondo y nadie pudo encontrar un mecanismo sensato para crear-valor-predeterminado-en-llamada. Un problema serio es que el alcance de la llamada es a menudo completamente diferente al de la definición, lo que hace que la resolución de nombres sea incierta si los valores predeterminados se evaluaron en el momento de la llamada. Un "cambio" significa "que puede lograr el fin deseado de la siguiente manera", no "esto es un error en el diseño de Python". holdenweb 3 oct 2017 a las 16:03
126

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 culpes si propongo algo insensible o imposible.

Siempre que los objetos de Python sean mutables , creo que esto debería tenerse en cuenta al diseñar los argumentos predeterminados. Cuando crea una instancia de una lista:

a = []

espera obtener una nueva lista referenciada por a.

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

def x(a=[]):

instanciar una nueva lista en la definición de función y no en la invocación? Es como si estuvieras preguntando "si el usuario no proporciona el argumento, entonces crea una nueva lista y úsala como si la hubiera producido la persona que llama". En cambio, creo que esto es ambiguo:

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

usuario, ¿desea establecer ade forma predeterminada 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 en la invocación de la función). Por otro lado, si el usuario quisiera el mapeo de 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):
7
  • 13
    Podría hacer: def x (a = None): Y luego, si a es None, establezca a = datetime.datetime.now ()Anon 16 de julio de 2009 a las 0:18
  • 25
    Gracias por esto. Realmente no podría decir por qué esto me molesta muchísimo. Lo ha 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 algunas veces ingenuamente "traduce" características del lenguaje, este falso amigo me pateó en el blando de la cabeza a lo grande, al igual que los atributos de clase. Entiendo por qué las cosas son así, pero no puedo evitar que me disguste, sin importar lo positivo que pueda resultar. Al menos es tan contrario a mi experiencia, que probablemente (con suerte) nunca lo olvidaré ...AndreasT 22/04/11 a las 9:33
  • 6
    @ Andreas, una vez que usa Python durante el tiempo suficiente, comienza a ver cuán lógico es que Python interprete las cosas como atributos de clase de la manera en que lo hace; es solo debido a las peculiaridades y limitaciones 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 memoria) reflejen su contenido (en codigo). Karl Knechtel 22/07/11 a las 19:55
  • 7
    La estructura normativa no es un capricho ni una 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 a los anarquistas: seguro que todo el mundo es libre, pero necesitas estructura para conseguir que alguien vacíe la basura y pavimente el camino. Supongo que soy viejo ... :)AndreasT 26/07/11 a las 8:54
  • 7
    La definición de la función se ejecuta en el momento de la 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 30/03/15 a las 11:28
88

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.

Compare 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 agregas cosas, se agrega a todas las instancias de esa clase. La razón es exactamente la misma.

Es simplemente "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 creen objetos.

Sí, es inesperado. Pero una vez que cae el centavo, encaja perfectamente con el funcionamiento de Python en general. De hecho, es una buena ayuda para la enseñanza, y una vez que comprenda por qué sucede esto, asimilará Python mucho mejor.

Dicho esto, debería ocupar un lugar destacado en cualquier buen tutorial de Python. Porque, como mencionas, todos se topan con este problema tarde o temprano.

5
  • ¿Cómo se define un atributo de clase que es diferente para cada instancia de una clase? Kieveli 15 de julio de 2009 a las 19:04
  • 20
    Si es diferente para cada instancia, no es un atributo de clase. Los atributos de clase son atributos de CLASS. De ahí el nombre. Por tanto, son los mismos para todos los casos. Lennart Regebro 15 de julio de 2009 a las 19:17
  • 2
    ¿Cómo se 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 sobre las variables miembros normales de una clase). Kieveli 16 de julio de 09 a las 14:03
  • 2
    @Kievieli: Estás hablando de variables miembros normales de una clase. :-) Puede definir atributos de instancia diciendo self.attribute = value en cualquier método. Por ejemplo __init __ (). Lennart Regebro 16 de julio de 09 a las 14:14
  • @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 7 de enero de 2012 a las 4:45
77

¿Por qué no haces una introspección?

Estoy realmente sorprendido de que nadie haya realizado la introspección perspicaz que ofrece Python ( 2y 3aplicar) en los llamables.

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 objeto de función en sí . Como mencionó 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 se aplica lo mismo (use __defaults__o func_defaultsen Python 2; sí, dos nombres para lo mismo).

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 incluirá en el __defaults__atributo del 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:

Ejecutemos ahora 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 la función. No hay nada extraño aquí, 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 obtiene una nueva lista vacía nueva si no se pasó ningún argumento a.


Para verificar aún más que la lista __defaults__es la misma que la utilizada en la función func, puede simplemente cambiar su función para devolver el idde la 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 estos 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 de que barse realice el proceso de creación de la función y vincularla al nombre .

3
  • 1
    ¿Es id(...)necesario para esa última verificación, o el isoperador respondería la misma pregunta? das-g 9 de marzo de 2016 a las 8:09
  • 1
    @ das-g funcionaría isbien, solo lo usé id(val)porque creo que podría ser más intuitivo. Dimitris Fasarakis Hilliard 9 de marzo de 2016 a las 8:20
  • El uso Nonecomo predeterminado limita severamente la utilidad de la __defaults__introspección, por lo que no creo que funcione bien como defensa de tener el __defaults__trabajo de la forma en que lo hace. La evaluación diferida haría más para mantener los valores predeterminados de funciones útiles de ambos lados. Brilliand 18/10/19 a las 5:32
64

Solía ​​pensar que crear los objetos en tiempo de ejecución sería el mejor enfoque. Ahora estoy menos seguro, ya que se pierden algunas funciones útiles, aunque puede valer la pena de todos modos simplemente para evitar la confusión de los novatos. Las desventajas de hacerlo son:

1. Desempeño

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

Si se usa la evaluación en tiempo de llamada, entonces se llama a la función costosa cada vez que se usa su función sin un argumento. O pagaría un precio caro en cada llamada o necesitaría almacenar en caché manualmente el valor externamente, contaminando su espacio de nombres y agregando verbosidad.

2. Forzar parámetros vinculados

Un truco útil es vincular los parámetros de una lambda a la vinculación actual de una variable cuando se crea la 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 vincularán ial valor de tiempo de llamada de i, por lo que obtendría una lista de funciones que devolvieron todas 9.

De lo contrario, la única forma de implementar esto 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

Considere el código:

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

Podemos obtener información sobre los argumentos y los 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 generación de documentos, metaprogramación, decoradores, etc.

Ahora, suponga que el comportamiento de los valores predeterminados podría cambiarse para que sea el equivalente de:

_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 hacer una introspección y ver cuáles son los argumentos predeterminados . Debido a que los objetos no han sido construidos, nunca podemos conseguirlos sin llamar a la función. Lo mejor que podemos hacer es almacenar el código fuente y devolverlo como una cadena.

8
  • 2
    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 simplemente llamará a esa función. yairchu 16 de julio de 2009 a las 10:24
  • @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 16 de julio de 2009 a las 10:59
  • 2
    @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 la evaluación de código arbitrario podría terminar teniendo un efecto. Brian 16 de julio de 2009 a las 11:02
  • 2
    Un diseño de lenguaje diferente a menudo solo significa escribir las cosas de manera diferente. Su primer ejemplo podría escribirse fácilmente como: _expensive = costoso (); def foo (arg = _expensive), si específicamente no desea que se reevalúe. Glenn Maynard 16 de julio de 09 a las 18:23
  • @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 16 de julio de 2009 a las 19:04
64

5 puntos en defensa de Python

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

  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 momento de la carga del módulo (y solo en el momento de la 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 utiliza negrita, que rara vez se aplica fuera de los encabezados. RTFM: Lea el manual fino.

  5. Metaaprendizaje : caer en la trampa es en realidad un momento muy útil (al menos si eres un aprendiz reflexivo), porque posteriormente comprenderás mejor el punto "Consistencia" anterior y eso te enseñará mucho sobre Python.

4
  • 21
    Me tomó un año descubrir que este comportamiento está arruinando mi código en producción, terminé eliminando una función completa hasta que me encontré con este defecto de diseño por casualidad. Estoy usando Django. Dado que el entorno de ensayo no tuvo muchas solicitudes, este error nunca tuvo ningún impacto en el control de calidad. Cuando comenzamos a trabajar y recibimos muchas solicitudes simultáneas, ¡algunas funciones de utilidad comenzaron a sobrescribir los parámetros de las demás! Haciendo agujeros de seguridad, errores y demás. oriadam 5 de septiembre de 2015 a las 13:09
  • 12
    @oriadam, no te ofendas, pero me pregunto cómo aprendiste Python sin encontrarte con esto antes. Estoy aprendiendo Python ahora y este posible error se menciona en el tutorial oficial de Python junto con la primera mención de los argumentos predeterminados. (Como se mencionó en el punto 4 de esta respuesta). Supongo que la moraleja es, bastante poco comprensiva, leer los documentos oficiales del lenguaje que usa para crear software de producción. Wildcard 30 de agosto de 2016 a las 2:26
  • 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 2 de septiembre de 2016 a las 13:26
  • 1
    @oriadam, su empresa necesita revisión de código y codificadores expertos reales en el lenguaje en el que escriben para cuando tengan entornos de desarrollo, puesta en escena y producción. Los errores de novato y los malos hábitos de código no deberían llegar al código de producciónRobin De Schepper 4 de enero a las 22:03
55

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 predeterminados
  2. todo se 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
3
  • (En realidad, agregar es un mal ejemplo, pero mi punto principal sigue siendo que los enteros sean inmutables)Anon 15 de julio de 2009 a las 23:54
  • Me di cuenta para mi disgusto después de comprobar que, con b establecido en [], b .__ add __ ([1]) devuelve [1] pero también deja b todavía [] a pesar de que las listas son mutables. Mi error. Anon 16 de julio de 2009 a las 0:03
  • @ANon: la hay __iadd__, pero no funciona con int. Por supuesto. :-)Veky 8 de mayo de 2014 a las 13:16
36

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

excepto en el 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 una respuesta esté ahí: efectivamente convertiría cada función con parámetros predeterminados en un cierre. Incluso si todo está oculto en el intérprete y no es un cierre completo, los datos deben almacenarse en algún lugar. Sería más lento y usaría más memoria.

4
  • 9
    No necesitaría ser un cierre, una mejor manera de pensarlo sería simplemente hacer que el código de bytes que crea los 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 del cuerpo. Brian 16 de julio de 2009 a las 9:39
  • 11
    Es cierto, pero aún así reduciría la velocidad de Python, y en realidad sería bastante sorprendente, a menos que haga lo mismo con 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 16 de julio de 2009 a las 11:49
  • De acuerdo con Lennart. Como le gusta decir a Guido, para cada característica de idioma o biblioteca estándar, hay alguien usándola. Jason Baker 16 de julio de 2009 a las 13:21
  • 8
    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 tal diferencia central de análisis tendría efectos radicales, y probablemente muchos oscuros, en el lenguaje en su conjunto. Glenn Maynard 16 de julio de 2009 a las 18:10
36

1) El llamado problema de "Argumento predeterminado mutable" es, en general, un ejemplo especial que demuestra que:
"Todas las funciones con este problema sufren también un problema de efectos secundarios similar en el parámetro real ",
que va en contra de las reglas de la programación funcional, generalmente indeseable y deben arreglarse ambos 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 segura es primero copyo deepcopyel objeto de entrada 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()o some_set.copy()o se pueden copiar fácilmente como somelist[:]o list(some_list). Cada objeto también puede ser copiado por copy.copy(any_object)o más completo por copy.deepcopy()(este último útil si el objeto mutable está compuesto por objetos mutables). Algunos objetos se basan fundamentalmente en efectos secundarios como el objeto "archivo" y no se pueden reproducir de forma 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 debe guardarse en ningún atributo público de una instancia devuelta por esta función. (Suponiendo que los atributos privados de instancia no deben modificarse desde fuera de esta clase o subclases por convención. Es decir, _var1es un atributo privado)

Conclusión:
Los objetos de parámetros de entrada no deben modificarse en su lugar (mutados) ni tampoco deben vincularse a un objeto devuelto por la función. (Si preferimos la programación sin efectos secundarios, lo cual se recomienda encarecidamente, consulte Wiki sobre "efectos secundarios" (los dos primeros párrafos son relevantes en este contexto).)

2)
Solo si se requiere el efecto secundario en el parámetro real pero no se desea en el parámetro predeterminado, 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 .

14
  • 6
    Espero que sepa que Python no es un lenguaje de programación funcional. Veky 8 de mayo de 2014 a las 13:18
  • 7
    Sí, Python es un lenguaje de múltiples paradigmas con algunas características funcionales. ("No hagas que todos los problemas parezcan un clavo solo porque tienes un martillo"). Muchos de ellos están en las mejores prácticas de Python. Python tiene un CÓMO interesante. Programación funcional. Otras características son los cierres y el curado, que no se mencionan aquí. hynekcer 8 de mayo de 2014 a las 15:54
  • 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 la 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 16/01/18 a las 14:27
  • 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 datos de manera irrazonable se considera un error y, por lo tanto, es poco común. hynekcer 17/01/18 a las 21:41
  • Estoy de acuerdo con esta respuesta. Y no entiendo por qué def f( a = None )se recomienda la construcción cuando realmente te refieres a otra cosa. Copiar está bien, porque no debes mutar argumentos. Y cuando lo hace if a is None: a = [1, 2, 3], copia la lista de todos modos. koddo 16 feb 2018 a las 16:15
30

En realidad, esto no tiene nada que ver con los valores predeterminados, aparte de que a menudo surge como un comportamiento inesperado cuando escribe 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 tiene exactamente el mismo problema.

El problema es que fooestá modificando una variable mutable pasada desde 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 como append_5; entonces la persona que llama estaría llamando 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 acepte un argumento predeterminado, y probablemente no devolvería la lista (dado 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 u obtuvo el valor predeterminado. Su código debe dejar los argumentos mutables solos a menos que esté claro por el contexto / nombre / documentación que se supone que los argumentos deben ser modificados. Usar valores mutables pasados ​​como argumentos como temporales 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, necesita hacer una copia.

11
  • 10
    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 24 de agosto de 2012 a las 12:27
  • 2
    @AndyHayden si se espera que la función modifique el argumento, ¿por qué tendría sentido tener un valor predeterminado? Mark Ransom 17/10/2017 a las 13:31
  • 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={}en él para completarlo. Mark Ransom 17/10/2017 a las 18:02
  • 1
    @AndyHayden El punto de mi respuesta es que si alguna vez se sorprende al mutar accidentalmente el valor predeterminado de un argumento, entonces tiene otro error, que es que su código puede mutar accidentalmente el valor de una persona que llama cuando no se usó el valor predeterminado . Y tenga en cuenta que usar Noney asignar el valor predeterminado real si el argumento es None no resuelve ese problema (lo considero un patrón anti por esa razón). Si soluciona el otro error evitando la mutación de los valores de los argumentos, tengan o no valores predeterminados, nunca notará ni se preocupará por este comportamiento "asombroso". Ben 17/10/2017 a las 21:44
  • 1
    @AndyHayden Y si lo está poniendo self.foo = foo.copy()allí de todos modos, ¿qué daño tiene si el valor predeterminado para fooes []? Es la copia que lo protege de problemas de argumentos mutables, sin establecer el valor predeterminado Nonecuando realmente desea un valor predeterminado de []. Claro que podría escribir if foo is None: self.foo = []; else: self.foo = foo.copy(), pero ¿por qué cuando podría reemplazar 4 líneas con una sola línea (que es una de las 4 que necesita de todos modos) y tener el valor real del argumento predeterminado más claro en la definición de la función? Ben 18/10/2017 a las 0:51
27

Ya es un tema muy 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
1
  • 3
    en realidad, esto puede ser un poco confuso para los recién llegados ya que las 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 modificada aaún puede tener la misma id(a). Jörn Hees 8/04/2017 a las 13:47
26

Es una optimización del rendimiento. Como resultado de esta funcionalidad, ¿cuál de estas dos llamadas a función 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 (consulte 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

I doubt the experienced behavior has a practical use (who really used static variables in C, without breeding bugs ?)

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 se trata de 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.

0
25

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 la función los usa varias veces, son y siguen siendo el mismo objeto.

Cuando son mutables, cuando se 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 la función se compila y se crea una 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 se hace referencia a ellos por

  • viendo que la lista se crea antes de que la función haya terminado de compilarse en un objeto de función,
  • observando que el id es el mismo cada vez que se hace referencia a la lista,
  • observando que la lista permanece cambiada cuando se llama por segunda vez a la función que la usa,
  • observando el orden en el 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 ejecutándolo 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']

¿Viola esto el principio de "menor asombro"?

Este orden de ejecución suele confundir a 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 que la instrucción habitual para los nuevos usuarios es crear sus argumentos predeterminados como este en su lugar:

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 obtenido o no un argumento diferente al predeterminado. Si no obtenemos ningún argumento, entonces realmente queremos usar una nueva lista vacía [], como predeterminada.

Como dice la sección del tutorial sobre el flujo de control :

If you don’t want the default to be shared between subsequent calls, you can write the function like this instead:

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

La respuesta más corta probablemente sería "la definición es ejecución", por lo tanto, todo el argumento no tiene sentido estricto. Como un ejemplo más elaborado, puede 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 ambas cosas.

Sin embargo, estoy de acuerdo en que es un problema cuando intentas usar constructores predeterminados.

22

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 (se explican bien en la respuesta aceptada).

El papel de (2) se ha cubierto ampliamente en este hilo. (1) es probablemente el factor que causa 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:

...all variables found outside of the innermost scope are read-only (an attempt to write to such a variable will simply create a new local variable in the innermost scope, leaving the identically named outer variable unchanged).

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 a la lista vacía según lo especificado por la firma cuando se crea una instancia de la función, y está disponible para lectura y escritura siempre que exista el objeto de función.

Llamar foosin anular un valor predeterminado utiliza ese valor predeterminado de foo.func_defs. En este caso, foo.func_defs[0]se usa para adentro del alcance del código del objeto de función. Cambios al 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 lenguajes , de modo que los valores predeterminados de la firma de la función se utilizan 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) , se 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 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.
  • Sobre L = [], la asignación no puede tener éxito en foo.func_defs[0], porque ese atributo es de solo lectura.
  • Según (1) , se crea una nueva variable local también nombrada Len el ámbito local y se utiliza para el resto de la llamada a la función. foo.func_defs[0]por lo tanto, permanece sin cambios para futuras invocaciones de foo.
21

Una solución simple usando Ninguno

>>> 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]
0
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 en lugar de cuando se ejecuta. Debido a que las listas y los diccionarios son mutables, cualquier alteración de este parámetro afectará a 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 usando el parámetro compartido de esta función al mismo tiempo ajenos 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 efectivo: 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)

The convention for achieving the desired result in Python is to provide a default value of None and to document the actual behaviour in the docstring.

Esta implementación asegura que cada llamada a la función reciba la lista predeterminada o la lista pasada 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' en los que el programador pretendía que se compartiera el parámetro de lista predeterminado, pero esto es más probable que sea la excepción que la regla.

0
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 su parámetro predeterminado y llámelo dentro de un bloque try para obtener el valor predeterminado (este es el tipo de cosas para las que sirve la abstracción lambda).

La segunda opción es buena porque los usuarios de la función pueden pasar un invocable, que puede que ya exista (como un type)

16

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

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

Feo, pero funciona.

3
  • 3
    Esta es una buena solución en los casos en los que está utilizando un software de generación automática de documentación para documentar los tipos de argumentos que espera la función. Poner a = Ninguno y luego establecer a en [] si a es Ninguno no ayuda al lector a comprender de un vistazo lo que se espera. Michael Scott Cuthbert 20 de enero de 2013 a las 6:55
  • Buena idea: volver a enlazar ese nombre garantiza que nunca se podrá modificar. Realmente me gusta eso. holdenweb 16/01/18 a las 14:29
  • 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, podrá modificarla como desee sin efectos secundarios inesperados. Mark Ransom 25 de mayo de 2018 a las 16:56
16

Cuando hacemos esto:

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

... asignamos el argumento aa una lista sin nombre , si la persona que llama no pasa el valor de a.

Para simplificar las cosas de esta discusión, démosle un nombre temporalmente 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, foose llama a un efecto que notamos la próxima vez sin especificarlo a.

Así que esto es lo que ves (recuerda, pavlose inicializa en []):

 >>> foo()
 [5]

Ahora pavloes [5].

Llamar de foo()nuevo modifica de pavlonuevo:

>>> foo()
[5, 5]

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

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

Entonces, pavloes todavía [5, 5].

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

A veces aprovecho este comportamiento como 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()

He usado esto para crear instancias de clases de cliente que acceden a recursos externos, y también para crear dictados o listas para memorización.

Dado que no creo que este patrón sea muy conocido, hago un breve comentario para evitar futuros malentendidos.

2
  • 2
    Prefiero agregar un decorador para la memorización y poner el caché de memorización en el objeto de función en sí. Stefano Borini 6 de febrero de 2015 a las 7:34
  • Este ejemplo no reemplaza el patrón más complejo que muestra, porque llama _make_singletona la hora def en el ejemplo de argumento predeterminado, pero a la hora de la llamada en el ejemplo global. Una verdadera sustitución usaría algún tipo de cuadro mutable para el valor predeterminado del argumento, pero la adición del argumento brinda la oportunidad de pasar valores alternativos. Yann Vernier 19/11/2017 a las 8:29
15

Puede ser cierto que:

  1. Alguien está usando todas las funciones de idiomas / bibliotecas, y
  2. Cambiar el comportamiento aquí no sería aconsejable, pero

es totalmente coherente mantener las dos 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 el 3, o hacen el punto 3 y restan importancia a los puntos 1 y 2. Pero los tres son ciertos.

Puede ser cierto que cambiar de caballos a mitad de camino 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 los aspectos internos de Python podría explicar un campo minado de consecuencias. Sin embargo,

El comportamiento existente no es Pythonic, y Python tiene éxito porque muy poco sobre el lenguaje viola el principio del menor asombro en cualquier lugar cercano.esto mal. Es un problema real, sea conveniente o no 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; aprendes 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 otros lenguajes. Los aficionados y los curiosos se vuelven Pythonistas cuando se asombran del poco tiempo que se tarda en hacer que algo funcione, no por un diseño fl, es decir, un rompecabezas lógico oculto, que va en contra de las intuiciones de los programadores que se sienten atraídos por Python. porque simplemente funciona .

10
  • 7
    -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 07/07/12 a las 19:24
  • 5
    Entonces, ¿es "sorprendentemente ignorante" decir que en Python tendría más sentido que un argumento predeterminado de [] permaneciera [] cada vez que se llama a la función? Christos Hayward 27/12/12 a las 22:09
  • 4
    Y es ignorante considerar como un modismo desafortunado establecer un argumento predeterminado en Ninguno, y luego en el cuerpo del cuerpo de la función establecer si argumento == Ninguno: argumento = []? ¿Es un ignorante considerar este modismo 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 tendrá un valor predeterminado de []? Christos Hayward 27/12/12 a las 22:11
  • 4
    Pero en Python, parte del espíritu del lenguaje es que no es necesario sumergirse demasiado; array.sort () funciona, y funciona independientemente de lo poco que entienda sobre ordenación, O grande y constantes. La belleza de Python en el mecanismo de clasificación de matrices, para dar uno de los innumerables ejemplos, es que no es necesario que se sumerja profundamente en los aspectos internos. Y para decirlo de otra manera, la belleza de Python es que normalmente no se requiere que uno se sumerja en profundidad en la implementación para obtener algo que simplemente funciona. Y hay una solución alternativa (... si argumento == Ninguno: argumento = []), FALLO. Christos Hayward 27/12/12 a las 22:41
  • 3
    Como 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 en sí la que se escondió, esa misma lista es lo único disponible para enlazar con x, ya sea que haya algo atascado dentro de ella o no. ¿Cómo podría ser de otra manera? Jerry B 5/10/2013 a las 6:18
9

¡Este "error" me dio muchas horas extras 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, todavía)

Les 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
1
  • 1
    Tu ejemplo no parece muy realista. ¿Por qué pasaría errorscomo un parámetro en lugar de empezar desde cero cada vez? Mark Ransom 25 de mayo a las 23:39
9

Esto no es un defecto de diseño . Cualquiera que se tropiece con esto está haciendo algo mal.

Hay 3 casos que veo en los que puede encontrarse con este problema:

  1. Tiene la intención de modificar el argumento como 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 lo modificó accidentalmente . Eso es un error, arréglalo.
  3. Tiene la intención de modificar el argumento para usarlo 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, ¡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 de la pregunta podría caer en la categoría 1 o 3. Es extraño que modifique la lista aprobada y la devuelva; deberías elegir uno u otro.

10
  • "Hacer algo mal" es el diagnóstico. Dicho esto, creo que hay momentos en los 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 solo para entrevistas, ¡en el código real que probablemente desee @lru_cache! Andy Hayden 17/10/2017 a las 18:19
  • 1
    Totalmente en desacuerdo, es absolutamente un defecto de diseño en muchos casos y no que el programador esté haciendo algo malaCuria 25 de mayo a las 10:44
  • @aCuria, ¿tienes un caso 4 que es diferente de los 3 que presenté? Me encantaría saberlo, cuéntame más. El comportamiento de Python puede no tener sentido en esta circunstancia, pero es muy útil en otros lugares y cambiarlo sería un desastre. Mark Ransom 25 de mayo a la 1:04 p.m.
  • Nunca me he encontrado con el problema del OP a pesar de que tiene tantos votos positivos, porque tener un argumento predeterminado que sea mutable es un diseño extraño para mí. qwr 25 de mayo a las 19:56
  • @MarkRansom Si damos por sentado que los efectos secundarios están bien, no hay nada de malo en modificar un argumento predeterminado como parte de una función de efectos secundarios. Digamos que tiene una función que hace algo con una lista y devuelve la lista. Queremos asegurarnos de que la función siempre devuelva una lista. Entonces, tener una lista vacía (o no vacía) como predeterminada tiene mucho sentido. El lenguaje está violando una gran proporción de las expectativas de los nuevos programadores de Python. ¿Por qué están mal y el idioma correcto? ¿Estaría argumentando lo contrario si el lenguaje tuviera el comportamiento opuesto? Clement Cherlin 3 de junio a las 14:05
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
0
8

Todas las demás respuestas explican por qué este es en realidad un comportamiento agradable y deseado, o por qué no debería necesitarlo de todos modos. La mía es para aquellos tercos que quieren ejercer su derecho a doblegar el lenguaje a su voluntad, 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 redefinamos 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 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 usar argumentos de palabras clave, así:

foo(a=[4])

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

0
7

TLDR: los valores predeterminados de tiempo definido son coherentes 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 namepieza debe evaluarse en el ámbito de definición name; después de todo, queremos estar disponibles allí. 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 bien, ¿cuándo evaluar default?

La coherencia ya dice "en la definición": todo lo demás def <name>(<args=defaults>):se evalúa mejor también en la definición. Retrasar algunas partes sería una 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 el momento de la 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
    ...
6
  • 1
    "La coherencia ya dice" en la definición ": todo lo demás def <name>(<args=defaults>):se evalúa mejor también 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 deban evaluarse en el mismo ámbito. defaultes algo diferente al resto de la línea: es una expresión. Evaluar una expresión es un proceso muy diferente a definir una función. LarsH 23/09/19 a las 14:38
  • Las definiciones de funciones @LarsH se evalúan en Python. Ya sea que sea de una declaración ( def) o expresión ( lambda) no cambia que crear una función signifique 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 23/09/19 a las 15:22
  • 1
    De acuerdo, crear una función significa evaluación en cierto 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 no se "evalúa" el cuerpo de la función (se analiza 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 ser evaluadas "completamente". LarsH 24/09/19 a las 15:09
  • 1
    No quiero decir que esté equivocado, solo que su conclusión no se deriva únicamente de la coherencia. LarsH 24/09/19 a las 15:09
  • Los valores predeterminados de @LarsH no son parte del cuerpo, ni estoy afirmando que la coherencia sea el único criterio. ¿Puedes hacer una sugerencia de cómo aclarar la respuesta? MisterMiyagi 24 sep 2019 a las 15:15
6

Creo que la respuesta a esta pregunta radica en cómo Python pasa datos a parámetros (pasan por valor o por referencia), no 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 números, y otro tipo de datos son objetos. En segundo lugar, al pasar datos a parámetros, Python pasa el tipo de datos elementales por valor, es decir, hace una copia local del valor a una variable local, pero pasa el objeto por referencia, es decir, punteros al objeto.

Admitiendo los dos puntos anteriores, expliquemos qué sucedió con el código Python. Es solo porque pasa por referencia para objetos, pero no tiene nada que ver con mutable / inmutable, o posiblemente el hecho de que la instrucción "def" se ejecuta solo una vez cuando está definida.

[] 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 mediante 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 . Al ejecutar el segundo foo (), lo que dice la página web de effbot (los elementos ya no se evalúan) es incorrecto. ase evalúa como el objeto de 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 forma.

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). Ninguno está en algún lugar del espacio, pero sabemos que está allí y solo hay una copia de Ninguno allí. Entonces, cada vez que se invoca foo, los elementos se evalúan (a diferencia de alguna respuesta que solo se evalúa una vez) para que sea Ninguno, para ser claros, la referencia (o la dirección) de Ninguno. Luego, en 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, digamos, 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, sigue siendo 11111111 Entonces viene foo (2, []). Aunque el [] en foo (2, []) tiene el mismo contenido que el parámetro predeterminado [] al llamar a foo (1), ¡su dirección es diferente! Dado que 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 está señalada por elementos. más. En consecuencia, un apéndice de 3 hará items[1,3].

De las explicaciones anteriores, podemos ver que la página web de effbot recomendada en la respuesta aceptada no dio una respuesta relevante a esta pregunta. Es más, creo que un punto de la página web de effbot está mal. Creo que el código con respecto a la UI.Button 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 dar 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]()dará 9, otro valor de i.

2
  • 5
    Tu último punto está mal. Pruébelo y verá que x[7]()es así 9. Duncan 2/10/2013 a las 13:29
  • 2
    "Python pasa tipo de datos elementales por valor, es decir, hace una copia local del valor a una variable local" es completamente incorrecto. Estoy asombrado de que alguien, obviamente, pueda conocer Python muy bien, y sin embargo, tenga un malentendido tan horrible de los fundamentos. :-(Veky 19/11/2014 a las 9:07