python language-design least-astonishment

python - "El menor asombro" y el argumento predeterminado mutable



language-design least-astonishment (30)

5 puntos en defensa de Python

  1. Simplicidad : el comportamiento es simple en el siguiente sentido: la mayoría de las personas caen en esta trampa solo una vez, no varias 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 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 de función.

  3. Utilidad : como señala Frederik Lundh en su explicación de "Valores de parámetros predeterminados en Python" , el comportamiento actual puede ser bastante útil para la programación avanzada. (Utilizar con moderación.)

  4. Documentación suficiente : en la documentación más básica de Python, el tutorial, el problema se anuncia en voz alta como una "Advertencia importante" en la primera subsección de la Sección "Más sobre la definición de funciones" . La advertencia incluso usa negrita, que rara vez se aplica fuera de los encabezados. RTFM: Lea el fino manual.

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

Cualquier problema con Python durante 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 esperan 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 lo 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í 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 hizo un ejemplo interesante. Junto con la mayoría de sus comentarios y los de Utaal en particular, elaboré más detalladamente:

>>> 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 el enlace dentro de la función significaría que x está efectivamente vinculado al valor predeterminado especificado cuando la función se llama, no se define, algo que presentaría un defecto profundo: la línea de def sería "híbrida" en el sentido de que parte del enlace (del objeto de función) ocurriría en la definición, y parte (asignación de parámetros predeterminados) en el momento de invocación de la función.

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


Arquitectura

Asignar valores predeterminados en una llamada de función es un olor de código.

def a(b=[]): pass

Esta es una firma de una función que no sirve para nada. No solo por los problemas descritos por otras respuestas. No voy a entrar en eso aquí.

Esta función tiene como objetivo hacer dos cosas. Cree una nueva lista y ejecute una funcionalidad, probablemente en dicha lista.

Las funciones que hacen dos cosas son funciones malas, ya que aprendemos de las prácticas de código limpio.

Atacando este problema con el polimorfismo, extenderíamos la lista de python o envolveríamos uno en una clase, luego realizaríamos nuestra función en él.

Pero espera que digas, me gustan mis frases.

Bien adivina que. El código es más que una forma de controlar el comportamiento del hardware. Es una forma de

  • Comunicándose con otros desarrolladores, trabajando en el mismo código.

  • poder cambiar el comportamiento del hardware cuando surgen nuevos requisitos.

  • poder entender el flujo del programa después de que retome el código luego de dos años para realizar el cambio mencionado anteriormente.

No te dejes las bombas de tiempo para recogerlas más tarde.

Separando esta función en las dos cosas que hace, necesitamos una clase.

class ListNeedsFives(object): def __init__(self, b=None): if b is None: b = [] self.b = b def foo(): self.b.append(5)

Ejecutado por

a = ListNeedsFives() a.foo() a.b

¿Y por qué esto es mejor que combinar todo el código anterior en una sola función?

def dontdothis(b=None): if b is None: b = [] b.append(5) return b

¿Por qué no hacer esto?

A menos que falle en su proyecto, su código vivirá. Lo más probable es que su función hará más que esto. La forma correcta de hacer un código mantenible es separar el código en partes atómicas con un alcance adecuadamente limitado.

El constructor de una clase es un componente muy comúnmente reconocido para cualquier persona que haya hecho Programación Orientada a Objetos. Colocar la lógica que maneja la creación de instancias de la lista en el constructor hace que la carga cognitiva de entender lo que hace el código sea menor.

El método foo()no devuelve la lista, ¿por qué no?

Al devolver una lista independiente, usted podría asumir que es seguro hacer lo que usted quiera. Pero puede que no lo sea, ya que también es compartido por el objeto a. Obligando al usuario a referirse a él ya que ables recuerda a dónde pertenece la lista. Cualquier código nuevo que quiera modificar ab, naturalmente, se colocará en la clase, donde pertenece.

La def dontdothis(b=None):función de firma no tiene ninguna de estas ventajas.


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 se utiliza por la función, varias veces por esa función, son y siguen siendo el mismo objeto.

Cuando son mutables, cuando están mutados (por ejemplo, agregándole 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 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 son referenciados 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 la función que la usa se llama una segunda vez,
  • observando el orden en que se imprime la salida desde la fuente (que convenientemente he numerado 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 "menos asombro"?

Este orden de ejecución es frecuentemente confuso para los nuevos usuarios de Python. Si entiendes 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 las instrucciones habituales para los nuevos usuarios son crear sus argumentos predeterminados como este:

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

Esto utiliza el singleton Ninguno como un objeto centinela para indicar a la función si hemos obtenido un argumento distinto al predeterminado. Si no obtenemos ningún argumento, entonces realmente queremos usar una nueva lista vacía [], como predeterminada.

Como dice la sección de tutoriales sobre flujo de control :

Si no desea que el valor predeterminado se comparta entre las llamadas subsiguientes, puede escribir la función así:

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


¿Por qué no haces una introspección?

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

Dada una pequeña función de func definida como:

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

Cuando Python lo encuentra, lo primero que hará es compilarlo para crear un objeto de code 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 a ahora 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.x para esto, para Python 2 se aplica lo mismo (use __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 ejecute esta definición, tomará todos los parámetros predeterminados especificados ( a = [] aquí) y los __defaults__ en el atributo __defaults__ para el objeto de función (sección relevante: Callables):

>>> func.__defaults__ ([],)

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

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

Ahora ejecutemos esta función:

>>> func()

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

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

Entonces, ahí lo tienen, la razón por la que ocurre este ''defecto'' , es porque los argumentos predeterminados son parte del objeto de función. No hay nada raro aquí, todo es un poco sorprendente.

La solución común para combatir esto es usar None como predeterminado y luego inicializar en el cuerpo de la función:

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

Dado que el cuerpo de la función se ejecuta de nuevo cada vez, siempre se obtiene una nueva lista vacía si no se pasa ningún argumento para a .

Para verificar más a fondo que la lista en __defaults__ es la misma que la que se usa en la función function, puede cambiar su función para devolver el id de la lista a usarlo dentro del cuerpo de la función. Luego, compárelo con la lista en __defaults__ (posición [0] en __defaults__ ) y verá cómo se están refiriendo 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 se dará cuenta, se llama a input() antes del proceso de compilación de la función y se vincula a la bar nombres.


AFAICS nadie ha publicado todavía la parte relevante de la documentation :

Los valores predeterminados de los parámetros 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 de entender cuando un parámetro predeterminado es un objeto mutable, como una lista o un diccionario: si la función modifica el objeto (por ejemplo, agregando 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 Ninguno como predeterminado, y probarlo explícitamente en el cuerpo de la función [...]


Bueno, la razón es simplemente que los enlaces se realizan cuando se ejecuta el código, y la definición de la función se ejecuta, bueno ... cuando se definen las funciones.

Compara esto:

class BananaBunch: bananas = [] def addBanana(self, banana): self.bananas.append(banana)

Este código sufre de 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 solo "Cómo funciona", y hacer que funcione de manera diferente en el caso de la función probablemente sea complicado, y en el caso de la clase probablemente sea imposible, o al menos ralentizar mucho la creación de instancias 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 el funcionamiento de Python en general. De hecho, es una buena ayuda para la enseñanza, y una vez que entiendas por qué sucede esto, desarrollarás Python mucho mejor.

Dicho esto, debería tener un lugar destacado en cualquier buen tutorial de Python. Porque como mencionas, todo el mundo se encuentra con este problema, tarde o temprano.


En realidad, esto no es un defecto de diseño, y no es por razones internas o de rendimiento.
Viene simplemente del hecho de que las funciones en Python son objetos de primera clase, y no solo una pieza de código.

Tan pronto como se llega a pensar de esta manera, tiene sentido: una función es un objeto que se evalúa en su definición; los parámetros predeterminados son una especie de "datos de miembro" 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 tener un mejor conocimiento de cómo funcionan los objetos funcionales.


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 - el nuevo objeto se imprime
  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

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 haya una respuesta allí mismo: 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.


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 que no sea comprensible 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 creas una instancia de una lista:

a = []

espera obtener una nueva lista a la que hace referencia un .

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

def x(a=[]):

¿crear una nueva lista en la definición de la función y no en la invocación? Es como si estuvieras preguntando "si el usuario no proporciona el argumento, crea una nueva lista y úsala como si hubiera sido generada por la persona que llama". Creo que esto es ambiguo en su lugar:

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

usuario, ¿desea que se establezca por defecto la fecha y hora correspondiente a cuando está definiendo o ejecutando x ? En este caso, como en el anterior, mantendré el mismo comportamiento como 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 deseara la asignación en tiempo de definición, podría escribir:

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

Lo sé, lo sé: eso es un cierre. Como alternativa, Python podría proporcionar una palabra clave para forzar el enlace en el tiempo de definición:

def x(static a=b):


Solía ​​pensar que crear los objetos en tiempo de ejecución sería el mejor enfoque. Ahora no estoy tan seguro, ya que pierdes algunas funciones útiles, aunque puede valer la pena independientemente, simplemente para evitar la confusión de los principiantes. Las desventajas de hacerlo son:

1. Rendimiento

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

Si se usa la evaluación de 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 costoso en cada llamada, o tendría que almacenar el valor manualmente en forma externa, contaminando su espacio de nombres y agregando verbosidad.

2. Forzando los parámetros enlazados

Un truco útil es vincular los parámetros de un lambda al enlace actual de una variable cuando se crea el lambda. Por ejemplo:

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

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

La única forma de implementar esto sería crear un cierre adicional con el enlace 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 módulo de inspect , que

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

Esta información es muy útil para cosas como la generación de documentos, metaprogramación, decoradores, etc.

Ahora, suponga que el comportamiento de los valores predeterminados podría cambiarse de modo 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 realizar una introspección y ver cuáles son los argumentos predeterminados. Debido a que los objetos no se han construido, nunca podemos obtenerlos sin llamar a la función. Lo mejor que podemos hacer es almacenar el código fuente y devolverlo como una cadena.


Supongamos que tienes el siguiente código

fruits = ("apples", "bananas", "loganberries") def eat(food=fruits): ...

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

Sin embargo, supuestamente más adelante en el código, hago algo como

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

luego, si los parámetros predeterminados estuvieran 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 las frutas habían sido cambiadas. Esto sería más sorprendente para la OMI que descubrir que su función foo anterior estaba mutando la lista.

El problema real reside en las variables mutables, y todos los idiomas tienen este problema en cierta medida. 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 utiliza el valor de la clave StringBuffer 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 usando un valor idéntico al que lo pusieron, o la persona que parece que no puede recuperar su objeto a pesar de que la clave que usan es literalmente la misma objeto que se usó para colocarlo en el mapa (esta es 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 un buen ejemplo de un caso en el que los recién llegados a Python serán sorprendidos y mordidos. Pero argumentaría que si "arreglamos" esto, entonces eso solo crearía una situación diferente donde serían mordidos en su lugar, y esa sería incluso menos intuitiva. Además, este es siempre el caso cuando se trata de variables mutables; siempre te encuentras con casos en los que alguien podría esperar intuitivamente uno o el comportamiento opuesto, dependiendo del código que estén escribiendo.

Personalmente, me gusta el enfoque actual de Python: los argumentos de la función predeterminada se evalúan cuando se define la función y ese objeto es siempre el predeterminado. Supongo que podrían usar un caso especial utilizando una lista vacía, pero ese tipo de carcasa especial causaría aún más asombro, por no mencionar que es incompatible con los anteriores.


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

Hay 3 casos que veo donde puedes encontrarte con este problema:

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

El ejemplo en la pregunta podría caer en la categoría 1 o 3. Es extraño que modifique la lista aprobada y la devuelva; Usted debe elegir uno o el otro.


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

singleton = None def use_singleton(): global singleton if singleton is None: singleton = _make_singleton() return singleton.use_me()

Si singletonsolo es usado por 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 clientes que acceden a recursos externos, y también para crear dictados o listas para la memorización.

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


Creo que la respuesta a esta pregunta radica en cómo Python pasa los datos al parámetro (pase por valor o por referencia), no a la mutabilidad o cómo Python maneja la declaración "def".

Una breve introduccion. Primero, hay dos tipos de tipos de datos en python, uno es un tipo de datos elemental simple, como números, y otro tipo de datos son objetos. En segundo lugar, al pasar datos a parámetros, Python pasa el tipo de datos elemental por valor, es decir, hace una copia local del valor a una variable local, pero pasa el objeto por referencia, es decir, los punteros al objeto.

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

[] es un objeto, por lo que Python pasa la referencia de [] a a, es decir, aes solo un puntero a [] que se encuentra en la memoria como un objeto. Sin embargo, solo hay una copia de [] con muchas referencias. Para el primer foo (), la lista [] se cambia a 1 por el método de adición. Pero tenga en cuenta que solo hay una copia del objeto de lista y este objeto ahora se convierte en 1 . Cuando se ejecuta el segundo foo (), lo que dice la página web de 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 manera.

Para validar aún más mi respuesta, echemos un vistazo a dos códigos adicionales.

====== No. 2 ========

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

[]es un objeto, así 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í. Por lo tanto, cada vez que se invoca foo, los elementos se evalúan (a diferencia de algunas respuestas que solo se evalúan una vez) para ser Ninguno, para ser claros, la referencia (o la dirección) de Ninguno. Luego, en el foo, el elemento se cambia a [], es decir, apunta a otro objeto que tiene una dirección diferente.

====== No. 3 =======

def foo(x, items=[]): items.append(x) return items foo(1) # returns [1] foo(2,[]) # returns [2] foo(3) # returns [1,3]

La invocación de foo (1) make items apunta a un objeto de lista [] con una dirección, por ejemplo, 11111111. el contenido de la lista se cambia a 1 en la función foo en la secuela, pero la dirección no se cambia, aún 11111111 Entonces viene foo (2, []). Aunque [] 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, itemstiene que tomar la dirección de este nuevo [], digamos 2222222, y devolverlo después de hacer algún cambio. Ahora se ejecuta foo (3). ya que soloxSe proporciona, los artículos 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 ningún elemento. Más. En consecuencia, un anexo de 3 hará items[1,3].

De las explicaciones anteriores, podemos ver que la página web de effbot recomendada en la respuesta aceptada no dio una respuesta relevante a esta pregunta. Además, creo que un punto en la página web de effbot es incorrecto. Creo que el código con respecto a la UI. Botón es correcto:

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

Cada botón puede contener una función de devolución de llamada distinta que mostrará un valor diferente de i. Puedo 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]()le daremos 9, otro valor de i.


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 para esta discusión, démosle temporalmente un nombre a la lista sin nombre. ¿Qué tal pavlo?

def foo(a=pavlo): ...

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

Si pavloes mutable (modificable), y footermina modificándolo, se notifica un efecto que notificamos la próxima vez foosin especificar a.

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

>>> foo() [5]

Ahora, pavloes [5].

Llamando de foo()nuevo modifica de pavlonuevo:

>>> foo() [5, 5]

La especificación de acuándo se foo()asegura la llamada pavlono se toca.

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

Por lo tanto, pavloes todavía [5, 5].

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


En realidad, esto no tiene nada que ver con los valores predeterminados, aparte de que a menudo aparece 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 obtienes exactamente el mismo problema.

El problema es que fooestá modificando una variable mutable transmitida por la persona que llama, cuando la persona que llama no espera esto. Código como este estaría bien si la función fuera llamada algo así append_5; entonces el llamante llamaría a la función para modificar el valor que pasan y se esperaría el comportamiento. Pero es muy poco probable que tal función tome un argumento predeterminado y probablemente no devuelva la lista (ya que la persona que llama ya tiene una referencia a esa lista; la que acaba de pasar).

Su original foo, con un argumento predeterminado, no debería estar modificando asi se pasó de forma explícita u obtuvo el valor predeterminado. Su código debe dejar los argumentos mutables solo a menos que esté claro en el contexto / nombre / documentación que se supone que los argumentos deben modificarse. Usar valores mutables pasados ​​como argumentos como temporarios locales es una idea extremadamente mala, ya sea que estemos en Python o no y si hay argumentos predeterminados involucrados o no.

Si necesita manipular destructivamente un temporal local en el curso de la computación de algo, y necesita comenzar su manipulación a partir de un valor de argumento, necesita hacer una copia.


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 eso
  2. Las funciones son objetos (se explican bien en la respuesta aceptada).

El rol de (2) ha sido cubierto ampliamente en este hilo. (1) es probablemente el factor causante del asombro, ya que este comportamiento no es "intuitivo" cuando se trata de otros idiomas.

(1) se describe en el tutorial de Python sobre las clases . En un intento de asignar un valor a un atributo de clase de solo lectura:

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

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 es, por lo tanto, un atributo de lectura-escritura de foo. Se inicializa en la lista vacía como se especifica en la firma cuando se crea una instancia de la función, y está disponible para leer y escribir mientras exista el objeto de la función.

Llamar foosin anular un valor predeterminado utiliza el valor predeterminado de foo.func_defs. En este caso, foo.func_defs[0]se utiliza para adentro del alcance del código del objeto de función. Cambios a acambio foo.func_defs[0], que son parte del fooobjeto y persisten entre la ejecución del código en foo.

Ahora, compare esto con el ejemplo de la documentación sobre la emulación del comportamiento de argumento predeterminado de otros idiomas , de manera que los valores predeterminados de la firma de 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 None, un objeto inmutable.
  • Cuando la función se ejecuta con valores predeterminados (sin ningún parámetro especificado Len la llamada de función), foo.func_defs[0]( None) está disponible en el ámbito local como L.
  • Una vez L = [], la asignación no puede tener éxito foo.func_defs[0], porque ese atributo es de solo lectura.
  • Por (1) , una nueva variable local también denominada Lse crea en el ámbito local y se usa para el resto de la llamada a la función. foo.func_defs[0]Así se mantiene sin cambios para futuras invocaciones de foo.

Las soluciones aquí son:

  1. Utilícelo Nonecomo su valor predeterminado (o nonce object), y enciéndalo 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 está la abstracción lambda).

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


Puede ser cierto que:

  1. Alguien está usando cada función de idioma / biblioteca, y
  2. Cambiar el comportamiento aquí sería desaconsejable, pero

es totalmente coherente mantener las dos características anteriores y hacer otro punto:

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

Las otras respuestas, o al menos algunas de ellas, hacen los puntos 1 y 2 pero no 3, o hacen el punto 3 y minimizan los puntos 1 y 2. Pero las tres son verdaderas.

Puede ser cierto que cambiar de caballo a mitad de la corriente aquí requeriría una rotura significativa, y que podría haber más problemas creados al cambiar Python para manejar intuitivamente el fragmento de apertura de Stefano. Y puede ser cierto que alguien que conocía bien los 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 de la lengua viola el principio de mínima sorpresa en cualquier lugar cercaesto mal Es un problema real, sea o no prudente desarraigarlo. Es un defecto de diseño. Si entiendes el lenguaje mucho mejor tratando de rastrear el comportamiento, puedo decir que C ++ hace todo esto y más; aprendes mucho navegando, por ejemplo, los errores de puntero sutil. Pero esto no es Pythonic: las personas que se preocupan por Python lo suficiente como para perseverar frente a este comportamiento son personas que se sienten atraídas por el lenguaje porque Python tiene muchas menos sorpresas que otro idioma. Dabblers y los curiosos se convierten en pitonistas cuando se asombran de lo poco que se tarda en hacer que algo funcione, no por un diseño de lógica, es decir, un rompecabezas de lógica oculta, que se opone a las intuiciones de los programadores que se sienten atraídos por Python. porque simplemente funciona .


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

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

Feo, pero funciona.


Solo cambia la función para ser:

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


Tema ya ocupado, pero por lo que leí aquí, lo siguiente me ayudó a darme cuenta de cómo está funcionando 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


Una solución simple utilizando 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]


Voy a demostrar una estructura alternativa para pasar un valor de lista predeterminado a una función (funciona igual de bien con los diccionarios).

Como otros han comentado extensamente, el parámetro de lista está vinculado a la función cuando se define y no cuando se ejecuta. Debido a que las listas y los diccionarios son mutables, cualquier alteración de este parámetro afectará otras llamadas a esta función. Como resultado, las llamadas subsiguientes a la función recibirán esta lista compartida que puede haber sido alterada por cualquier otra llamada a la función. Peor aún, dos parámetros están utilizando el parámetro compartido de esta función al mismo tiempo ajeno a los cambios realizados por el otro.

Método incorrecto (probablemente ...) :

def foo(list_arg=[5]): return list_arg a = foo() a.append(6) >>> a [5, 6] b = foo() b.append(7) # The value of 6 appended to variable ''a'' is now part of the list held by ''b''. >>> b [5, 6, 7] # Although ''a'' is expecting to receive 6 (the last element it appended to the list), # it actually receives the last element appended to the shared list. # It thus receives the value 7 previously appended by ''b''. >>> a.pop() 7

Puedes verificar que son el mismo objeto usando id:

>>> id(a) 5347866528 >>> id(b) 5347866528

"Effective Python: 59 maneras específicas de escribir mejor Python" de Brett Slatkin, ítem 20: uso Noney cadenas de texto para especificar argumentos predeterminados dinámicos (p. 48)

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

Esta implementación garantiza 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'', por lo que el programador pretende que se comparta el parámetro de lista predeterminado, pero es más probable que esta sea la excepción que la regla.


¡Este "error" me dio muchas horas extras de trabajo! Pero estoy empezando a ver un uso potencial de esto (pero me hubiera gustado que estuviera en el momento de la ejecución, todavía)

Te voy a 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) El llamado problema del "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 similares en el parámetro real ,"
Eso va en contra de las reglas de la programación funcional, Por lo general indeseable y deben fijarse ambos a la vez.

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 copyo deepcopyel objeto de entrada primero y luego hacer lo que sea con la copia.

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

Muchos tipos mutables incorporados tienen un método de copia como some_dict.copy()o some_set.copy()o se pueden copiar fácilmente como somelist[:]o list(some_list). Cada objeto también puede ser copiado copy.copy(any_object)o más completo copy.deepcopy()(el último es útil si el objeto mutable está compuesto de objetos mutables). Algunos objetos se basan fundamentalmente en efectos secundarios como el objeto "archivo" y no pueden reproducirse de forma significativa por copia. copying

Ejemplo de problema para una pregunta SO similar

class Test(object): # the original problematic class def __init__(self, var1=[]): self._var1 = var1 somevar = [1, 2] # an example without a default parameter t1 = Test(somevar) t2 = Test(somevar) t1._var1.append([1]) print somevar # [1, 2, [1]] but usually expected [1, 2] print t2._var1 # [1, 2, [1]] but usually expected [1, 2]

No se debe guardar ni en ningún atributo público de una instancia devuelta por esta función. (Suponiendo que los atributos privados de la 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 deben estar vinculados en un objeto devuelto por la función. (Si preferimos la programación sin efectos secundarios, 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 deseado en el parámetro predeterminado, la solución útil es def ...(var1=None): if var1 is None: var1 = [] More..

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


Es una optimización del rendimiento. Como resultado de esta funcionalidad, ¿cuál de estas dos llamadas de función crees que es más rápida?

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

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

# 1

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

# 2

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

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

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 toma mucho tiempo para construirse. Además, ten en cuenta que Python no es C. En C tienes constantes que son bastante libres. En Python no tienes este beneficio.


La respuesta más corta probablemente sería "la definición es la ejecución", por lo tanto, todo el argumento no tiene sentido estricto. Como un ejemplo más artificial, puedes citar esto:

def a(): return [] def b(x=a()): print x

Esperemos que sea suficiente para demostrar que no ejecutar las expresiones de argumento predeterminadas en el momento de la ejecución de la defdeclaración no es fácil o no tiene sentido, o ambos.

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


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