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




language-design least-astonishment (20)

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.


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.


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.


¿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 [...]


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

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.


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.


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.


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 .


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

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

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.





least-astonishment