Pregunta "Menos asombro" y el argumento predeterminado mutable


Cualquiera que haya jugueteado con Python el tiempo suficiente ha sido mordido (o hecho pedazos) por el siguiente problema:

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

Los principiantes de Python esperarían que esta función siempre devuelva 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 su primer encuentro con esta característica, y la llamó "un defecto de diseño dramático" 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í) la siguiente pregunta: ¿cuál es el motivo 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 errores de reproducción?)

Editar:

Baczek hizo un ejemplo interesante. Junto con la mayoría de sus comentarios y de Utaal en particular, elaboré más:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Para mí, parece que la decisión de diseño fue relativa a dónde poner el alcance de los parámetros: dentro de la función o "junto" con ella?

Hacer el enlace dentro de la función significaría que x está efectivamente vinculado al valor predeterminado especificado cuando se llama a la función, no definida, algo que presentaría un defecto profundo: el def line sería "híbrido" en el sentido de que parte de la vinculación (del objeto de función) ocurriría en definición, y parte (asignación de parámetros por defecto) en el tiempo de invocación de función.

El comportamiento real es más consistente: todo de esa línea se evalúa cuando se ejecuta esa línea, es decir, en la definición de la función.


2049
2017-07-15 18:00


origen


Respuestas:


En realidad, esto no es un defecto de diseño, y no es por el funcionamiento interno o el rendimiento.
Se debe simplemente al hecho de que las funciones en Python son objetos de primera clase y no solo una pieza de código.

En cuanto se llega a pensar de esta manera, tiene sentido por completo: una función es un objeto que se evalúa según su definición; los parámetros predeterminados son tipo de "datos de miembros" y, por lo tanto, su estado puede cambiar de una llamada a la 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 predeterminados de parámetros en Python.
Lo encontré muy claro, y realmente sugiero leerlo para un mejor conocimiento de cómo funcionan los objetos de función.


1349
2017-07-17 21:29



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, supongo más adelante en el código, hago algo así como

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

luego, si los parámetros predeterminados estaban vinculados a la ejecución de la función en lugar de a la declaración de la función, me sorprendería (de una manera muy mala) descubrir que los frutos habían cambiado. Esta sería una OMI más sorprendente que descubrir que su foola función anterior estaba mutando la lista.

El verdadero problema reside 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 StringBuffer clave cuando se colocó en el mapa, o almacena la clave por referencia? De cualquier manera, alguien está asombrado; ya sea la persona que intentó sacar el objeto del Map utilizando un valor idéntico al que lo colocaron, o la persona que aparentemente no puede recuperar su objeto a pesar de que la clave que están usando es literalmente el mismo objeto que se usó para ponerlo en el mapa (esto es en realidad, Python no permite que sus tipos de datos incorporados mutables se usen como claves de diccionario).

Su ejemplo es uno bueno en el que los recién llegados de Python se sorprenderán y morderán. Pero yo diría que si "arreglamos" esto, entonces eso solo crearía una situación diferente en la que serían mordidos en su lugar, y ese sería aún menos intuitivo. 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 una u otra conducta según el código que está escribiendo.

Personalmente me gusta el enfoque actual de Python: los argumentos de las funciones predeterminadas se evalúan cuando se define la función y ese objeto siempre es el predeterminado. Supongo que podrían utilizar un caso especial con una lista vacía, pero ese tipo de envoltura especial causaría aún más asombro, sin mencionar que sería incompatible.


231
2017-07-15 18:11



AFAICS nadie ha publicado la parte relevante de la documentación:

Los valores de los parámetros predeterminados se evalúan 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 para comprender cuándo 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 utilizar None como el valor por defecto y probarlo explícitamente en el cuerpo de la función [...]


195
2017-07-10 14:50



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 insostenible o imposible.

Siempre que los objetos de pitón son mutables Creo que esto debería tenerse en cuenta al diseñar los argumentos por defecto. Cuando crea una instancia de una lista:

a = []

esperas obtener un nuevo lista referenciada por un.

¿Por qué debería 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 preguntaras "si el usuario no proporciona el argumento, entonces instanciado una nueva lista y úsala 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, ¿quieres? un por defecto a 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 () invocó la invocación de función). Por otro lado, si el usuario quisiera el mapeo de definición de tiempo, 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 del tiempo de definición:

def x(static a=b):

97
2017-07-15 23:21



Bueno, la razón es simplemente que las consolidaciones se hacen 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 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 función probablemente sería complicado, y en el caso de clase probablemente imposible, o al menos ralentizar mucho la instanciación de objetos, ya que tendría que mantener el código de clase alrededor y ejecutarlo cuando se crean objetos.

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

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


72
2017-07-15 18:54



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 características útiles, aunque puede valer la pena sin importar simplemente para evitar la confusión de los novatos. 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. O bien pagaría un precio caro en cada llamada, o bien 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 un lambda al corriente enlace de una variable cuando se crea 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 i al tiempo de llamada valor de i, por lo que obtendría una lista de funciones que todos devolvieron 9.

La única manera de implementar esto sería crear un cierre adicional con el i bound, 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 los valores predeterminados utilizando el inspect mó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, supongamos 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 vemos cuáles son los argumentos predeterminados son. Debido a que los objetos no han sido construidos, no podemos alcanzarlos sin llamar a la función. Lo mejor que podemos hacer es almacenar el código fuente y devolverlo como una cadena.


50
2017-07-16 10:05



5 puntos en defensa de Python

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

  2. Consistencia: Python siempre pasa objetos, no nombres. El parámetro predeterminado es, obviamente, parte de la función encabezado (no el cuerpo de la función). Por lo tanto, debe ser evaluado 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 tiempo de 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 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 tema se anuncia en voz alta como un "Advertencia importante" en el primero subsección de la Sección "Más sobre definir funciones". La advertencia incluso usa negrita, que rara vez se aplica fuera de los encabezados. RTFM: lea el fino manual.

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


47
2018-03-30 11:18



¿Por qué no haces una introspección?

estoy De Verdad sorprendido, nadie ha realizado la perspicaz introspección ofrecida por Python (2 y 3 aplicar) en callables.

Dada una pequeña función simple func definido como:

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

Cuando Python lo encuentra, lo primero que hará es compilarlo para crear un code objeto para esta función. Mientras se completa este paso de compilación, Pitón evalúa* y entonces víveres los argumentos por defecto (una lista vacía) [] aquí) en el objeto de función en sí. Como la respuesta principal mencionó: la lista a ahora se puede considerar una 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 el objeto de función. Estoy usando Python 3.x para esto, para Python 2 se aplica lo mismo (uso __defaults__ o func_defaults en 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 ejecuta esta definición, tomará los parámetros predeterminados especificados (a = [] aquí y meterlos en el __defaults__ atributo para el objeto de función (sección relevante: Callables):

>>> func.__defaults__
([],)

O.k, entonces una lista vacía como entrada única en __defaults__, tal como se esperaba

Función después de la ejecución:

Vamos a ejecutar ahora esta función:

>>> func()

Ahora, veamos esos __defaults__ de 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 incrustado list objeto:

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

Entonces, ahí lo tienes, la razón por la cual esto 'falla' sucede, es porque los argumentos predeterminados son parte del objeto de función. No pasa nada raro aquí, todo es un poco sorprendente.

La solución común para combatir esto es a lo usual None como el 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 si no se aprobó ningún argumento para a.


Para verificar aún más que la lista en __defaults__ es el mismo que el utilizado en la función func simplemente puede cambiar su función para devolver el id de la lista a utilizado 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 habrás notado, input() se llama antes del proceso de construcción de la función y lo vincula al nombre bar está hecho.


42
2017-12-09 07:13



Este comportamiento es fácil de explicar 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

Asi que:

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 es creación a partir del valor predeterminado e impresa
  3. c cambios - la operación se realiza en el mismo objeto - y se imprime

40
2017-07-15 19:15



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é allí: convertiría efectivamente todas las funciones con parámetros predeterminados en un cierre. Incluso si todo está oculto en el intérprete y no es un cierre en toda regla, los datos deben almacenarse en algún lugar. Sería más lento y usaría más memoria.


30
2017-07-15 20:18



1) El llamado problema de "Argumento por defecto mutable" es en general un ejemplo especial que demuestra que:
"Todas las funciones con este problema sufrir también de un problema similar de efectos secundarios en el parámetro real, "
Eso va en contra de las reglas de la programación funcional, por lo general, indeseable y debe corregirse en conjunto.

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: un dupdo
Una solución absolutamente segura es copy o deepcopy el 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() o some_set.copy()o se puede 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() (el último útil si el objeto mutable se compone de 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 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 debería ser ni guardado en ningún público atributo de una instancia devuelta por esta función. (Asumiendo que privado los atributos de instancia no deberían modificarse fuera de esta clase o subclases por convención. es decir _var1 es un atributo privado)

Conclusión:
Los objetos de los parámetros de entrada no se deben modificar en su lugar (mutados) ni tampoco deben estar enlazados en un objeto devuelto por la función. (Si preferimos la programación sin efectos secundarios, que es muy recomendable. Ver Wiki sobre "efecto secundario" (Los dos primeros párrafos son relevantes en este contexto). .)

2)
Solo si el efecto secundario en el parámetro real es necesario pero no deseado en el parámetro predeterminado, entonces la solución más útil es def ...(var1=None):  if var1 is None:  var1 = []  Más..

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


29
2017-11-22 18:09