¿Buenos usos para los valores predeterminados de los argumentos de la función mutable?

82

Es un error común en Python establecer un objeto mutable como el valor predeterminado de un argumento en una función. Aquí hay un ejemplo tomado de este excelente artículo de David Goodger :

>>> def bad_append(new_item, a_list=[]):
        a_list.append(new_item)
        return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

La explicación de por qué sucede esto está aquí .

Y ahora mi pregunta: ¿Existe un buen caso de uso para esta sintaxis?

Quiero decir, si todos los que lo encuentran cometen el mismo error, lo depuran, comprenden el problema y, a partir de ahí, intentan evitarlo, ¿de qué sirve esa sintaxis?

Jonathan
fuente
1
La mejor explicación que conozco para esto está en la pregunta vinculada: las funciones son objetos de primera clase, al igual que las clases. Las clases tienen datos de atributos mutables; las funciones tienen valores predeterminados mutables.
Katriel
10
Este comportamiento no es una "elección de diseño", es el resultado de la forma en que funciona el lenguaje, partiendo de principios de funcionamiento simples, con el menor número posible de excepciones. En algún momento para mí, cuando comencé a "pensar en Python", este comportamiento se volvió natural, y me sorprendería si no sucediera
Jsbueno
2
Yo también me he preguntado esto. Este ejemplo está en toda la web, pero simplemente no tiene sentido: o desea mutar la lista pasada y tener una predeterminada no tiene sentido, o desea devolver una nueva lista y debe hacer una copia de inmediato. al ingresar a la función. No puedo imaginar el caso en el que sea útil hacer ambas cosas.
Mark Ransom
2
Acabo de encontrar un ejemplo más realista que no tiene el problema del que me quejo anteriormente. El valor predeterminado es un argumento para la __init__función de una clase, que se establece en una variable de instancia; esto es algo perfectamente válido para querer hacer, y todo sale horriblemente mal con un valor predeterminado mutable. stackoverflow.com/questions/43768055/…
Mark Ransom

Respuestas:

61

Puede usarlo para almacenar en caché valores entre llamadas a funciones:

def get_from_cache(name, cache={}):
    if name in cache: return cache[name]
    cache[name] = result = expensive_calculation()
    return result

pero generalmente ese tipo de cosas se hace mejor con una clase, ya que luego puede tener atributos adicionales para borrar el caché, etc.

Duncan
fuente
12
... o un decorador de memorias.
Daniel Roseman
29
@functools.lru_cache(maxsize=None)
Katriel
3
@katrielalex lru_cache es nuevo en Python 3.2, por lo que no todos pueden usarlo.
Duncan
2
Para su información, ahora hay backports.functools_lru_cache pypi.python.org/pypi/backports.functools_lru_cache
Panda
1
lru_cacheno está disponible si tiene valores sin hash.
Synedraacus
14

La respuesta canónica es esta página: http://effbot.org/zone/default-values.htm

También menciona 3 casos de uso "buenos" para el argumento predeterminado mutable:

  • vincular la variable local al valor actual de la variable externa en una devolución de llamada
  • caché / memorización
  • revinculación local de nombres globales (para código altamente optimizado)
Peter M. - representa a Monica
fuente
12

Quizás no mutes el argumento mutable, pero esperas un argumento mutable:

def foo(x, y, config={}):
    my_config = {'debug': True, 'verbose': False}
    my_config.update(config)
    return bar(x, my_config) + baz(y, my_config)

(Sí, sé que puede usarlo config=()en este caso particular, pero lo encuentro menos claro y menos general).

Reincorporar a Monica
fuente
2
También asegúrese de no mutar y no devolver este valor predeterminado directamente desde la función, de lo contrario, algún código fuera de la función puede mutarlo y afectará a todas las llamadas a la función.
Andrey Semakin
10
import random

def ten_random_numbers(rng=random):
    return [rng.random() for i in xrange(10)]

Utiliza el randommódulo, efectivamente un singleton mutable, como su generador de números aleatorios predeterminado.

Fred Foo
fuente
7
Pero este tampoco es un caso de uso muy importante.
Evgeni Sergeev
3
Creo que no hay diferencia en el comportamiento entre el "obtener referencia una vez" de Python y el "buscar randomuna vez por llamada de función" de no-Python . Ambos terminan usando el mismo objeto.
nyanpasu64
4

EDITAR (aclaración): El problema del argumento predeterminado mutable es un síntoma de una elección de diseño más profunda, es decir, que los valores del argumento predeterminado se almacenan como atributos en el objeto de función. Podría preguntar por qué se hizo esta elección; como siempre, estas preguntas son difíciles de responder correctamente. Pero ciertamente tiene buenos usos:

Optimización para el rendimiento:

def foo(sin=math.sin): ...

Tomando valores de objeto en un cierre en lugar de la variable.

callbacks = []
for i in range(10):
    def callback(i=i): ...
    callbacks.append(callback)
Katriel
fuente
7
los enteros y las funciones incorporadas no son mutables.
Reincorporar a Monica
2
@Jonathan: Todavía no hay un argumento predeterminado mutable en el ejemplo restante, ¿o simplemente no lo veo?
Reincorporar a Monica
2
@Jonathan: mi punto no es que sean mutables. Es que el sistema que Python usa para almacenar argumentos predeterminados, en el objeto de función, definido en tiempo de compilación, puede ser útil. Esto implica el problema del argumento predeterminado mutable, ya que reevaluar el argumento en cada llamada de función hará que el truco sea inútil.
Katriel
2
@katriealex: OK, pero dígalo en su respuesta que asume que los argumentos tendrían que ser reevaluados y que muestra por qué eso sería malo. Nit-pick: los valores predeterminados de los argumentos no se almacenan en tiempo de compilación, sino cuando se ejecuta la declaración de definición de función.
Reincorporación a Monica
@WolframH: cierto: ¡P! Aunque los dos suelen coincidir.
Katriel
-1

En respuesta a la pregunta de los buenos usos de los valores de argumento predeterminados mutables, ofrezco el siguiente ejemplo:

Un valor predeterminado mutable puede ser útil para programar comandos fáciles de usar e importables de su propia creación. El método predeterminado mutable equivale a tener variables estáticas privadas en una función que puede inicializar en la primera llamada (muy parecido a una clase) pero sin tener que recurrir a globales, sin tener que usar un contenedor y sin tener que instanciar un objeto de clase que se importó. Es elegante a su manera, como espero que estén de acuerdo.

Considere estos dos ejemplos:

def dittle(cache = []):

    from time import sleep # Not needed except as an example.

    # dittle's internal cache list has this format: cache[string, counter]
    # Any argument passed to dittle() that violates this format is invalid.
    # (The string is pure storage, but the counter is used by dittle.)

     # -- Error Trap --
    if type(cache) != list or cache !=[] and (len(cache) == 2 and type(cache[1]) != int):
        print(" User called dittle("+repr(cache)+").\n >> Warning: dittle() takes no arguments, so this call is ignored.\n")
        return

    # -- Initialize Function. (Executes on first call only.) --
    if not cache:
        print("\n cache =",cache)
        print(" Initializing private mutable static cache. Runs only on First Call!")
        cache.append("Hello World!")
        cache.append(0)
        print(" cache =",cache,end="\n\n")
    # -- Normal Operation --
    cache[1]+=1 # Static cycle count.
    outstr = " dittle() called "+str(cache[1])+" times."
    if cache[1] == 1:outstr=outstr.replace("s.",".")
    print(outstr)
    print(" Internal cache held string = '"+cache[0]+"'")
    print()
    if cache[1] == 3:
        print(" Let's rest for a moment.")
        sleep(2.0) # Since we imported it, we might as well use it.
        print(" Wheew! Ready to continue.\n")
        sleep(1.0)
    elif cache[1] == 4:
        cache[0] = "It's Good to be Alive!" # Let's change the private message.

# =================== MAIN ======================        
if __name__ == "__main__":

    for cnt in range(2):dittle() # Calls can be loop-driven, but they need not be.

    print(" Attempting to pass an list to dittle()")
    dittle([" BAD","Data"])
    
    print(" Attempting to pass a non-list to dittle()")
    dittle("hi")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the private mutable value from the outside.")
    # Even an insider's attempt to feed a valid format will be accepted
    # for the one call only, and is then is discarded when it goes out
    # of scope. It fails to interrupt normal operation.
    dittle([" I am a Grieffer!\n (Notice this change will not stick!)",-7]) 
    
    print(" Calling dittle() normally once again.")
    dittle()
    dittle()

Si ejecuta este código, verá que la función dittle () se internaliza en la primera llamada pero no en llamadas adicionales, usa una caché estática privada (la predeterminada mutable) para el almacenamiento estático interno entre llamadas, rechaza los intentos de secuestro el almacenamiento estático, es resistente a la entrada maliciosa y puede actuar en función de las condiciones dinámicas (en este caso, en la cantidad de veces que se ha llamado a la función).

La clave para usar valores predeterminados mutables no es hacer nada que reasigne la variable en la memoria, sino cambiar siempre la variable en su lugar.

Para ver realmente el potencial y la utilidad de esta técnica, guarde este primer programa en su directorio actual con el nombre "DITTLE.py", luego ejecute el siguiente programa. Importa y usa nuestro nuevo comando dittle () sin requerir ningún paso para recordar o programar aros para saltar.

Aquí está nuestro segundo ejemplo. Compile y ejecute esto como un programa nuevo.

from DITTLE import dittle

print("\n We have emulated a new python command with 'dittle()'.\n")
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

¿No es eso lo más resbaladizo y limpio posible? Estos valores predeterminados mutables pueden ser realmente útiles.

========================

Después de reflexionar sobre mi respuesta por un tiempo, no estoy seguro de haber marcado la diferencia entre usar el método predeterminado mutable y la forma habitual de lograr lo mismo con claridad.

La forma habitual es utilizar una función importable que envuelva un objeto Class (y utilice un global). Entonces, para comparar, aquí hay un método basado en clases que intenta hacer las mismas cosas que el método predeterminado mutable.

from time import sleep

class dittle_class():

    def __init__(self):
        
        self.b = 0
        self.a = " Hello World!"
        
        print("\n Initializing Class Object. Executes on First Call only.")
        print(" self.a = '"+str(self.a),"', self.b =",self.b,end="\n\n")
    
    def report(self):
        self.b  = self.b + 1
        
        if self.b == 1:
            print(" Dittle() called",self.b,"time.")
        else:
            print(" Dittle() called",self.b,"times.")
        
        if self.b == 5:
            self.a = " It's Great to be alive!"
        
        print(" Internal String =",self.a,end="\n\n")
            
        if self.b ==3:
            print(" Let's rest for a moment.")
            sleep(2.0) # Since we imported it, we might as well use it.
            print(" Wheew! Ready to continue.\n")
            sleep(1.0)

cl= dittle_class()

def dittle():
    global cl
    
    if type(cl.a) != str and type(cl.b) != int:
        print(" Class exists but does not have valid format.")
        
    cl.report()

# =================== MAIN ====================== 
if __name__ == "__main__":
    print(" We have emulated a python command with our own 'dittle()' command.\n")
    for cnt in range(2):dittle() # Call can be loop-driver, but they need not be.
    
    print(" Attempting to pass arguments to dittle()")
    try: # The user must catch the fatal error. The mutable default user did not. 
        dittle(["BAD","Data"])
    except:
        print(" This caused a fatal error that can't be caught in the function.\n")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the Class variable from the outside.")
    cl.a = " I'm a griefer. My damage sticks."
    cl.b = -7
    
    dittle()
    dittle()

Guarde este programa basado en clases en su directorio actual como DITTLE.py y luego ejecute el siguiente código (que es el mismo que antes).

from DITTLE import dittle
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

Al comparar los dos métodos, las ventajas de usar un valor predeterminado mutable en una función deberían ser más claras. El método predeterminado mutable no necesita globales, sus variables internas no se pueden configurar directamente. Y aunque el método mutable aceptó un argumento aprobado con conocimiento para un solo ciclo y luego lo ignoró, el método Class se alteró permanentemente porque su variable interna está directamente expuesta al exterior. ¿En cuanto a qué método es más fácil de programar? Creo que eso depende de su nivel de comodidad con los métodos y la complejidad de sus objetivos.

usuario10637953
fuente