Gute Verwendung für Standardwerte für veränderbare Funktionsargumente?

82

In Python ist es ein häufiger Fehler, ein veränderbares Objekt als Standardwert für ein Argument in einer Funktion festzulegen. Hier ist ein Beispiel aus diesem hervorragenden Artikel von 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']

Die Erklärung, warum dies geschieht, ist hier .

Und nun zu meiner Frage: Gibt es einen guten Anwendungsfall für diese Syntax?

Ich meine, wenn jeder, der darauf stößt, den gleichen Fehler macht, ihn debuggt, das Problem versteht und von da an versucht, es zu vermeiden, welchen Nutzen hat eine solche Syntax?

Jonathan
quelle
1
Die beste Erklärung, die ich dafür kenne, ist die verknüpfte Frage: Funktionen sind erstklassige Objekte, genau wie Klassen. Klassen haben veränderbare Attributdaten. Funktionen haben veränderbare Standardwerte.
Katriel
10
Dieses Verhalten ist keine "Design-Wahl" - es ist ein Ergebnis der Funktionsweise der Sprache - ausgehend von einfachen Arbeitsprinzipien, mit so wenigen Ausnahmen wie möglich. Irgendwann, als ich anfing, in Python zu denken, wurde dieses Verhalten einfach natürlich - und ich wäre überrascht, wenn es nicht passieren würde
jsbueno
2
Ich habe mich auch gefragt. Dieses Beispiel ist im gesamten Web verfügbar, macht aber keinen Sinn. Entweder möchten Sie die übergebene Liste mutieren, und eine Standardeinstellung ist nicht sinnvoll, oder Sie möchten eine neue Liste zurückgeben und sofort eine Kopie erstellen beim Aufrufen der Funktion. Ich kann mir den Fall nicht vorstellen, in dem es nützlich ist, beides zu tun.
Mark Ransom
2
Ich bin gerade auf ein realistischeres Beispiel gestoßen, das nicht das Problem hat, über das ich mich oben beschwere. Der Standardwert ist ein Argument für die __init__Funktion einer Klasse, die in eine Instanzvariable gesetzt wird. Dies ist eine absolut gültige Sache, die man tun möchte, und bei einem veränderlichen Standard geht alles schrecklich schief. stackoverflow.com/questions/43768055/…
Mark Ransom

Antworten:

61

Sie können es verwenden, um Werte zwischen Funktionsaufrufen zwischenzuspeichern:

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

Aber normalerweise wird so etwas mit einer Klasse besser gemacht, da Sie dann zusätzliche Attribute haben können, um den Cache usw. zu leeren.

Duncan
quelle
12
... oder ein auswendig lernender Dekorateur.
Daniel Roseman
29
@functools.lru_cache(maxsize=None)
Katriel
3
@katrielalex lru_cache ist neu in Python 3.2, daher kann es nicht jeder verwenden.
Duncan
2
Zu Ihrer Information
Panda
1
lru_cacheist nicht verfügbar, wenn Sie nicht verwertbare Werte haben.
Synedraacus
13

Die kanonische Antwort lautet auf dieser Seite: http://effbot.org/zone/default-values.htm

Außerdem werden 3 "gute" Anwendungsfälle für veränderbare Standardargumente erwähnt:

  • Binden der lokalen Variablen an den aktuellen Wert der äußeren Variablen in einem Rückruf
  • Cache / Memoization
  • lokales erneutes Binden globaler Namen (für hochoptimierten Code)
Peter M. - steht für Monica
quelle
12

Vielleicht mutieren Sie das veränderbare Argument nicht, sondern erwarten ein veränderliches Argument:

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

(Ja, ich weiß, dass Sie config=()in diesem speziellen Fall verwenden können, aber ich finde das weniger klar und weniger allgemein.)

Stellen Sie Monica wieder her
quelle
2
Stellen Sie außerdem sicher, dass Sie diesen Standardwert nicht mutieren und nicht direkt von der Funktion zurückgeben. Andernfalls kann Code außerhalb der Funktion ihn mutieren und wirkt sich auf alle Funktionsaufrufe aus.
Andrey Semakin
10
import random

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

Verwendet das randomModul, effektiv einen veränderlichen Singleton, als Standard-Zufallszahlengenerator.

Fred Foo
quelle
7
Dies ist jedoch auch kein besonders wichtiger Anwendungsfall.
Evgeni Sergeev
3
Ich denke, es gibt keinen Unterschied im Verhalten zwischen Pythons "Referenz einmal erhalten" und Nicht-Pythons " randomeinmal pro Funktionsaufruf suchen". Beide verwenden dasselbe Objekt.
Nyanpasu64
4

BEARBEITEN (Klarstellung): Das Problem mit veränderlichen Standardargumenten ist ein Symptom für eine tiefere Entwurfsauswahl, nämlich dass Standardargumentwerte als Attribute im Funktionsobjekt gespeichert werden. Sie könnten fragen, warum diese Wahl getroffen wurde; Wie immer sind solche Fragen schwer richtig zu beantworten. Aber es hat sicherlich gute Verwendungsmöglichkeiten:

Leistungsoptimierung:

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

Abrufen von Objektwerten in einem Abschluss anstelle der Variablen.

callbacks = []
for i in range(10):
    def callback(i=i): ...
    callbacks.append(callback)
Katriel
quelle
7
Ganzzahlen und eingebaute Funktionen sind nicht veränderbar!
Stellen Sie Monica
2
@ Jonathan: Im verbleibenden Beispiel gibt es noch kein veränderbares Standardargument, oder sehe ich es einfach nicht?
Stellen Sie Monica
2
@ Jonathan: Mein Punkt ist nicht, dass diese veränderlich sind. Das System, mit dem Python Standardargumente auf dem zur Kompilierungszeit definierten Funktionsobjekt speichert, kann nützlich sein. Dies impliziert das Problem der veränderlichen Standardargumente, da eine Neubewertung des Arguments bei jedem Funktionsaufruf den Trick unbrauchbar macht.
Katriel
2
@katriealex: OK, aber bitte sagen Sie in Ihrer Antwort, dass Sie davon ausgehen, dass Argumente neu bewertet werden müssten, und dass Sie zeigen, warum das schlecht wäre. Nit-pick: Standardargumentwerte werden nicht zur Kompilierungszeit gespeichert, sondern wenn die Funktionsdefinitionsanweisung ausgeführt wird.
Stellen Sie Monica
@ WolframH: wahr: P! Obwohl die beiden oft zusammenfallen.
Katriel
-1

Als Antwort auf die Frage nach guten Verwendungsmöglichkeiten für veränderbare Standardargumentwerte biete ich das folgende Beispiel an:

Eine veränderbare Standardeinstellung kann nützlich sein, um benutzerfreundliche, importierbare Befehle Ihrer eigenen Erstellung zu programmieren. Die veränderbare Standardmethode besteht darin, private, statische Variablen in einer Funktion zu haben, die Sie beim ersten Aufruf initialisieren können (ähnlich wie bei einer Klasse), ohne jedoch auf Globals zurückgreifen zu müssen, ohne einen Wrapper verwenden zu müssen und ohne a instanziieren zu müssen Klassenobjekt, das importiert wurde. Es ist auf seine Weise elegant, wie ich hoffe, dass Sie zustimmen werden.

Betrachten Sie diese beiden Beispiele:

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

Wenn Sie diesen Code ausführen, werden Sie feststellen, dass die Funktion dittle () beim ersten Aufruf verinnerlicht wird, jedoch nicht bei zusätzlichen Aufrufen. Sie verwendet einen privaten statischen Cache (die veränderbare Standardeinstellung) für die interne statische Speicherung zwischen Aufrufen und lehnt Entführungsversuche ab Der statische Speicher ist widerstandsfähig gegen böswillige Eingaben und kann auf der Grundlage dynamischer Bedingungen (hier abhängig davon, wie oft die Funktion aufgerufen wurde) handeln.

Der Schlüssel zur Verwendung veränderlicher Standardeinstellungen besteht darin, nichts zu tun, was die Variable im Speicher neu zuweist, sondern die Variable immer an Ort und Stelle zu ändern.

Speichern Sie dieses erste Programm in Ihrem aktuellen Verzeichnis unter dem Namen "DITTLE.py" und führen Sie das nächste Programm aus, um die potenzielle Leistungsfähigkeit und Nützlichkeit dieser Technik wirklich zu erkennen. Es importiert und verwendet unseren neuen Befehl dittle (), ohne dass Schritte zum Erinnern oder Programmieren von Reifen zum Durchspringen erforderlich sind.

Hier ist unser zweites Beispiel. Kompilieren Sie dieses und führen Sie es als neues Programm aus.

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

Ist das nicht so glatt und sauber wie möglich? Diese veränderlichen Standardeinstellungen können sehr nützlich sein.

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

Nachdem ich eine Weile über meine Antwort nachgedacht habe, bin ich mir nicht sicher, ob ich den Unterschied zwischen der Verwendung der veränderlichen Standardmethode und der regulären Methode, dasselbe zu erreichen, klar gemacht habe.

Normalerweise wird eine importierbare Funktion verwendet, die ein Klassenobjekt umschließt (und ein globales Objekt verwendet). Zum Vergleich hier eine klassenbasierte Methode, die versucht, die gleichen Aktionen wie die veränderbare Standardmethode auszuführen.

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

Speichern Sie dieses klassenbasierte Programm in Ihrem aktuellen Verzeichnis als DITTLE.py und führen Sie dann den folgenden Code aus (der mit dem vorherigen identisch ist).

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

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

Durch den Vergleich der beiden Methoden sollten die Vorteile der Verwendung eines veränderlichen Standards in einer Funktion klarer werden. Die veränderbare Standardmethode benötigt keine Globalen, ihre internen Variablen können nicht direkt festgelegt werden. Und während die veränderbare Methode ein sachkundiges übergebenes Argument für einen einzelnen Zyklus akzeptierte und es dann abschüttelte, wurde die Class-Methode dauerhaft geändert, da ihre interne Variable direkt nach außen verfügbar ist. Welche Methode ist einfacher zu programmieren? Ich denke, das hängt von Ihrem Komfortniveau mit den Methoden und der Komplexität Ihrer Ziele ab.

user10637953
quelle