Frage "Wenigstens Verwunderung" und das Veränderbare Standardargument


Wer lange genug mit Python bastelte, wurde von folgendem Problem gebissen (oder in Stücke gerissen):

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

Python-Neulinge würden erwarten, dass diese Funktion immer eine Liste mit nur einem Element zurückgibt: [5]. Das Ergebnis ist stattdessen sehr anders und sehr erstaunlich (für einen Anfänger):

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

Ein Manager von mir hatte seine erste Begegnung mit diesem Feature und nannte es "einen dramatischen Designfehler" der Sprache. Ich antwortete, dass das Verhalten eine grundlegende Erklärung hatte, und es ist in der Tat sehr rätselhaft und unerwartet, wenn Sie die Interna nicht verstehen. Allerdings konnte ich folgende Frage nicht beantworten: Was ist der Grund für die Bindung des Standardarguments an die Funktionsdefinition und nicht an die Funktionsausführung? Ich bezweifle, dass das erfahrene Verhalten einen praktischen Nutzen hat (wer hat wirklich statische Variablen in C verwendet, ohne Fehler zu züchten?)

Bearbeiten:

Baczek machte ein interessantes Beispiel. Zusammen mit den meisten Ihrer Kommentare und insbesondere Utaals habe ich weiter ausgearbeitet:

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

Für mich scheint es, dass die Design-Entscheidung relativ dazu war, wo der Umfang der Parameter liegt: innerhalb der Funktion oder "zusammen" damit?

Das Binden innerhalb der Funktion würde das bedeuten x ist effektiv an den angegebenen Standard gebunden, wenn die Funktion aufgerufen wird, nicht definiert, etwas, das einen tiefen Fehler darstellen würde: die def Zeile wäre "hybrid" in dem Sinne, dass ein Teil der Bindung (des Funktionsobjekts) bei der Definition und ein Teil (Zuweisung von Standardparametern) zur Funktionsaufrufzeit passieren würde.

Das tatsächliche Verhalten ist konsistenter: Alles aus dieser Zeile wird ausgewertet, wenn diese Zeile ausgeführt wird, dh bei der Funktionsdefinition.


2049
2017-07-15 18:00


Ursprung


Antworten:


Eigentlich ist das kein Konstruktionsfehler, und das liegt nicht an den Interna oder der Leistung.
Es kommt einfach von der Tatsache, dass Funktionen in Python erstklassige Objekte und nicht nur ein Stück Code sind.

Sobald man darüber nachdenkt, ergibt das völlig Sinn: Eine Funktion ist ein Objekt, das nach seiner Definition bewertet wird; Standardparameter sind eine Art von "Mitgliedsdaten" und daher kann sich ihr Zustand von einem Aufruf zum anderen ändern - genau wie in jedem anderen Objekt.

Auf jeden Fall hat Effbot eine sehr schöne Erklärung der Gründe für dieses Verhalten in Standard-Parameterwerte in Python.
Ich fand es sehr klar, und ich empfehle wirklich, es zu lesen, um besser zu wissen, wie Funktionsobjekte funktionieren.


1349
2017-07-17 21:29



Angenommen, Sie haben den folgenden Code

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Wenn ich die Essensdeklaration sehe, ist es am wenigsten erstaunlich zu denken, dass, wenn der erste Parameter nicht gegeben ist, er dem Tupel gleich ist ("apples", "bananas", "loganberries")

Ich vermute jedoch später im Code, ich mache etwas wie

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

Wenn dann die Standardparameter eher an die Funktionsausführung als an die Funktionsdeklaration gebunden wären, wäre ich (auf sehr schlechte Weise) erstaunt, dass die Früchte verändert worden wären. Das wäre erstaunlicher IMO als das zu entdecken fooFunktion oben mutierte die Liste.

Das eigentliche Problem liegt bei veränderlichen Variablen, und alle Sprachen haben dieses Problem in gewissem Maße. Hier ist eine Frage: Angenommen, in Java habe ich den folgenden Code:

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?

Nun, verwendet meine Karte den Wert von StringBuffer Schlüssel, wenn es in der Karte platziert wurde, oder speichert es den Schlüssel als Referenz? So oder so, jemand ist erstaunt; entweder die Person, die versucht hat, das Objekt aus der Map Verwenden Sie einen Wert, der dem entspricht, mit dem Sie ihn eingeben, oder die Person, die ihr Objekt scheinbar nicht abrufen kann, obwohl der Schlüssel, den sie verwenden, buchstäblich das gleiche Objekt ist, mit dem sie in die Karte eingefügt wurde tatsächlich, warum Python nicht zulässt, dass seine veränderlichen eingebauten Datentypen als Wörterbuchschlüssel verwendet werden).

Ihr Beispiel ist ein guter Fall, in dem Python-Neulinge überrascht und gebissen werden. Aber ich würde argumentieren, wenn wir das "fixieren", dann würde das nur eine andere Situation schaffen, in der sie stattdessen gebissen werden würden, und das wäre noch weniger intuitiv. Darüber hinaus ist dies immer der Fall bei veränderbaren Variablen; Sie stoßen immer auf Fälle, in denen jemand intuitiv ein oder das entgegengesetzte Verhalten erwarten könnte, abhängig davon, welchen Code sie schreiben.

Ich persönlich mag Pythons derzeitigen Ansatz: Standardfunktionsargumente werden ausgewertet, wenn die Funktion definiert ist und dieses Objekt immer der Standard ist. Ich nehme an, sie könnten Sonderfälle mit einer leeren Liste machen, aber diese besondere Hülle würde noch mehr Erstaunen hervorrufen, ganz zu schweigen davon, dass sie rückwärtskompatibel sind.


231
2017-07-15 18:11



AFAICS hat noch niemand den relevanten Teil der Dokumentation:

Standard-Parameterwerte werden beim Ausführen der Funktionsdefinition ausgewertet. Dies bedeutet, dass der Ausdruck einmal ausgewertet wird, wenn die Funktion definiert ist und dass für jeden Aufruf der gleiche "vorberechnete" Wert verwendet wird. Dies ist besonders wichtig, um zu verstehen, wenn ein Standardparameter ein veränderbares Objekt ist, beispielsweise eine Liste oder ein Wörterbuch: Wenn die Funktion das Objekt modifiziert (z. B. durch Anhängen eines Elements an eine Liste), wird der Standardwert wirksam geändert. Dies ist im Allgemeinen nicht das, was beabsichtigt war. Um dies zu umgehen, verwenden Sie None als Standard und testen Sie explizit im Body der Funktion nach [...]


195
2017-07-10 14:50



Ich weiß nichts über die inneren Abläufe des Python-Interpreters (und ich bin auch kein Experte für Compiler und Interpreter), also beschuldige mich nicht, wenn ich irgendetwas Unsinniges oder Unmögliches vorschlage.

Vorausgesetzt, dass Python-Objekte sind veränderbar Ich denke, dass dies beim Entwurf der Standardargumente berücksichtigt werden sollte. Wenn Sie eine Liste instanziieren:

a = []

Sie erwarten, ein zu bekommen Neu Liste referenziert von ein.

Warum sollte das a = [] in

def x(a=[]):

instanziieren Sie eine neue Liste auf Funktionsdefinition und nicht auf Aufruf? Es ist genau so, als würden Sie fragen: "Wenn der Benutzer das Argument nicht zur Verfügung stellt instanziieren eine neue Liste und verwende sie so, als ob sie vom Anrufer erzeugt worden wäre ". Ich denke, das ist mehrdeutig:

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

Benutzer, willst du ein Standardmäßig wird die Datetime festgelegt, die dem Zeitpunkt der Definition oder Ausführung entspricht x? In diesem Fall, wie in der vorherigen, werde ich das gleiche Verhalten beibehalten, als ob das Standardargument "Zuweisung" die erste Anweisung der Funktion (datetime.now () beim Aufruf der Funktion war). Auf der anderen Seite, wenn der Benutzer die Definition-Zeit-Zuordnung wollte, könnte er schreiben:

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

Ich weiß, ich weiß: das ist eine Schließung. Alternativ kann Python ein Schlüsselwort bereitstellen, um die Definitionszeitbindung zu erzwingen:

def x(static a=b):

97
2017-07-15 23:21



Nun, der Grund ist ganz einfach, dass Bindungen ausgeführt werden, wenn Code ausgeführt wird, und die Funktionsdefinition wird ausgeführt, also ... wenn die Funktionen definiert sind.

Vergleiche das:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Dieser Code leidet unter genau dem gleichen unerwarteten Zufall. Bananen ist ein Klassenattribut, und wenn Sie also Dinge hinzufügen, wird es allen Instanzen dieser Klasse hinzugefügt. Der Grund ist genau der gleiche.

Es ist einfach "Wie es funktioniert", und es würde im Klassenfall anders funktionieren. Das wäre wahrscheinlich kompliziert und im Klassenfall wahrscheinlich unmöglich oder würde zumindest die Objektinstanziierung verlangsamen, da Sie den Klassencode herum behalten müssten und führe es aus, wenn Objekte erstellt werden.

Ja, es ist unerwartet. Aber sobald der Groschen fällt, passt es perfekt zu Python im Allgemeinen. In der Tat ist es eine gute Lehrhilfe, und wenn Sie erst einmal verstanden haben, warum das passiert, werden Sie Python viel besser verstehen.

Das heißt, es sollte in jedem guten Python-Tutorial eine wichtige Rolle spielen. Denn wie Sie schon erwähnt haben, laufen alle früher oder später auf dieses Problem ein.


72
2017-07-15 18:54



Ich dachte, dass das Erstellen der Objekte zur Laufzeit der bessere Ansatz wäre. Ich bin jetzt weniger sicher, da du einige nützliche Funktionen verlierst, obwohl es sich lohnt, egal ob man Neulinge verwirrt oder nicht. Die Nachteile dabei sind:

1. Leistung

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

Wenn die Anrufzeitauswertung verwendet wird, wird die teure Funktion jedes Mal aufgerufen, wenn Ihre Funktion ohne ein Argument verwendet wird. Sie würden entweder bei jedem Anruf einen teuren Preis zahlen oder den Wert manuell extern cachen, wodurch Ihr Namespace verschmutzt und Ausführlichkeit hinzugefügt wird.

2. Erzwingen gebundener Parameter

Ein nützlicher Trick ist es, Parameter eines Lambda an die zu binden Strom Bindung einer Variablen, wenn das Lambda erstellt wird. Beispielsweise:

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

Dies gibt eine Liste von Funktionen zurück, die jeweils 0,1,2,3 ... zurückgeben. Wenn das Verhalten geändert wird, binden sie stattdessen i zum Zeit für Anrufe Wert von i, so dass Sie eine Liste von Funktionen erhalten würden, die alle zurückgegeben wurden 9.

Die einzige Möglichkeit, dies anders zu implementieren, wäre, einen weiteren Abschluss mit der i-Grenze zu erstellen, dh:

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

3. Introspektion

Betrachten Sie den Code:

def foo(a='test', b=100, c=[]):
   print a,b,c

Wir können Informationen über die Argumente und Voreinstellungen mit Hilfe der inspect Modul, das

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

Diese Information ist sehr nützlich für Dinge wie Dokumentengenerierung, Metaprogrammierung, Dekorateure usw.

Nehmen wir jetzt an, das Verhalten der Standardwerte könnte geändert werden, so dass dies äquivalent ist zu:

_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=[]

Wir haben jedoch die Fähigkeit verloren, uns selbst zu inspizieren und die Standardargumente zu sehen sind. Da die Objekte nicht konstruiert wurden, können wir sie niemals erreichen, ohne die Funktion tatsächlich aufzurufen. Das Beste, was wir tun können, ist, den Quellcode abzuspeichern und als String zurückzugeben.


50
2017-07-16 10:05



5 Punkte zur Verteidigung von Python

  1. Einfachheit: Das Verhalten ist im folgenden Sinne einfach: Die meisten Menschen fallen nur einmal und nicht mehrmals in diese Falle.

  2. Konsistenz: Python immer übergibt Objekte, keine Namen. Der Standardparameter ist offensichtlich ein Teil der Funktion Überschrift (nicht der Funktionskörper). Es sollte daher bewertet werden zur Modulladezeit (und nur zur Modulladezeit, wenn nicht verschachtelt), nicht zur Funktionsaufrufzeit.

  3. Nützlichkeit: Wie Frederik Lundh in seiner Erklärung betont von "Standard-Parameterwerte in Python", das Das aktuelle Verhalten kann für fortgeschrittene Programmierung sehr nützlich sein. (Verwenden Sie sparsam.)

  4. Ausreichende Dokumentation: In der grundlegendsten Python-Dokumentation Im Tutorial wird das Thema lautstark als angekündigt ein "Wichtige Warnung" in dem zuerst Unterabschnitt des Abschnitts "Mehr zum Definieren von Funktionen". Die Warnung verwendet sogar fett, was selten außerhalb von Überschriften angewendet wird. RTFM: Lesen Sie das Handbuch.

  5. Meta-Lernen: Fallen in die Falle ist eigentlich ein sehr hilfreicher Moment (zumindest wenn Sie ein reflektierender Lerner sind), weil du den Punkt später besser verstehen wirst "Konsistenz" oben und das wird lerne viel über Python.


47
2018-03-30 11:18