3396 Stimmen

"Least Astonishment" und das Argument des veränderlichen Standards

Jeder, der lange genug an Python herumgebastelt hat, wurde von dem folgenden 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 jedoch ganz anders und (für einen Anfänger) sehr erstaunlich:

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

Ein Manager von mir hatte einmal seine erste Begegnung mit dieser Funktion und nannte sie einen "dramatischen Designfehler" der Sprache. Ich entgegnete ihm, dass es für dieses Verhalten eine Erklärung gibt, und dass es in der Tat sehr verwirrend und unerwartet ist, wenn man die Interna nicht versteht. Allerdings konnte ich mir die folgende Frage nicht beantworten: Warum wird das Standardargument bei der Funktionsdefinition und nicht bei der Funktionsausführung gebunden? Ich bezweifle, dass das erfahrene Verhalten einen praktischen Nutzen hat (wer hat wirklich statische Variablen in C verwendet, ohne Bugs zu erzeugen?)

Editar :

Baczek gibt ein interessantes Beispiel . Zusammen mit den meisten Ihrer Kommentare und Utaal's im Besonderen 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]

Ich habe den Eindruck, dass die Design-Entscheidung sich darauf bezog, wo der Bereich der Parameter platziert werden sollte: innerhalb der Funktion oder "zusammen" mit ihr?

Wenn die Bindung innerhalb der Funktion erfolgt, würde dies bedeuten, dass x ist beim Aufruf der Funktion effektiv an die angegebene Vorgabe gebunden und nicht definiert, was einen schwerwiegenden 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) beim Funktionsaufruf erfolgen würde.

Das tatsächliche Verhalten ist konsistenter: alles in dieser Zeile wird ausgewertet, wenn diese Zeile ausgeführt wird, also bei der Funktionsdefinition.

81 Stimmen

9 Stimmen

Ich habe keinen Zweifel daran, dass veränderbare Argumente das Prinzip des geringsten Erstaunens für eine durchschnittliche Person verletzen, und ich habe gesehen, wie Anfänger dort hineingetreten sind und dann heldenhaft Mailinglisten durch Mailing-Tupel ersetzt haben. Nichtsdestotrotz sind veränderbare Argumente immer noch im Einklang mit Python Zen (Pep 20) und fallen unter die Klausel "obvious for Dutch" (verstanden/ausgenutzt von Hardcore-Python-Programmierern). Der empfohlene Workaround mit doc string ist der beste, aber der Widerstand gegen doc strings und jegliche (geschriebene) Doku ist heutzutage nicht mehr so ungewöhnlich. Ich persönlich würde einen Dekorator bevorzugen (sagen wir @fixed_defaults).

6 Stimmen

Mein Argument, wenn ich darauf stoße, ist: "Warum müssen Sie eine Funktion erstellen, die eine Variable zurückgibt, die optional eine Variable sein kann, die Sie an die Funktion übergeben würden? Entweder wird eine Mutable geändert oder eine neue erstellt. Warum muss man beides mit einer Funktion machen? Und warum sollte der Interpreter so umgeschrieben werden, dass man das tun kann, ohne drei Zeilen zum Code hinzuzufügen?" Weil es hier darum geht, die Art und Weise, wie der Interpreter mit Funktionsdefinitionen und Evokationen umgeht, neu zu schreiben. Das ist eine Menge Arbeit für einen kaum notwendigen Anwendungsfall.

12voto

Flimm Punkte 112964

Ja, dies ist ein Konstruktionsfehler in Python

Ich habe alle anderen Antworten gelesen und bin nicht überzeugt. Dieser Entwurf verstößt gegen das Prinzip des geringsten Erstaunens.

Die Vorgaben hätten so gestaltet werden können, dass sie beim Aufruf der Funktion ausgewertet werden, anstatt bei der Definition der Funktion. So macht es Javascript:

function foo(a=[]) {
  a.push(5);
  return a;
}
console.log(foo()); // [5]
console.log(foo()); // [5]
console.log(foo()); // [5]

Ein weiterer Beweis dafür, dass es sich hierbei um einen Konstruktionsfehler handelt, ist, dass die Entwickler des Python-Kerns derzeit über die Einführung einer neuen Syntax zur Behebung dieses Problems diskutieren. Siehe diesen Artikel: Standardwerte für spät gebundene Argumente in Python .

9voto

Norfeldt Punkte 6202

Dieser "Bug" hat mir eine Menge Überstunden beschert! Aber ich fange an, eine mögliche Verwendung dafür zu sehen (aber ich hätte es trotzdem gerne zur Ausführungszeit gehabt)

Ich werde Ihnen ein Beispiel geben, das ich für nützlich halte.

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

druckt das Folgende

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

2 Stimmen

Ihr Beispiel scheint nicht sehr realistisch zu sein. Warum würden Sie errors als Parameter zu verwenden, anstatt jedes Mal von vorne zu beginnen?

8voto

rassa45 Punkte 3400

Ändern Sie einfach die Funktion zu sein:

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

0 Stimmen

Damit ist die Frage aber noch nicht beantwortet.

8voto

Mark Ransom Punkte 283960

Dies ist kein Konstruktionsfehler . Jeder, der darüber stolpert, macht etwas falsch.

Ich sehe 3 Fälle, in denen dieses Problem auftreten kann:

  1. Sie beabsichtigen, das Argument als Nebeneffekt der Funktion zu ändern. In diesem Fall ist es macht nie Sinn um ein Standardargument zu haben. Die einzige Ausnahme ist, wenn Sie die Argumentliste missbrauchen, um Funktionsattribute zu haben, z. B. cache={} und es wird nicht erwartet, dass Sie die Funktion überhaupt mit einem tatsächlichen Argument aufrufen.
  2. Sie beabsichtigen, das Argument unverändert zu lassen, aber Sie haben versehentlich hizo ändern. Das ist ein Fehler, beheben Sie ihn.
  3. Sie beabsichtigen, das Argument zur Verwendung innerhalb der Funktion zu ändern, erwarten aber nicht, dass die Änderung außerhalb der Funktion sichtbar ist. In diesem Fall müssen Sie eine kopieren. des Arguments, ob es nun die Vorgabe war oder nicht! Python ist keine Call-by-Value-Sprache, so dass es die Kopie nicht für Sie macht, müssen Sie explizit über sie sein.

Das Beispiel in der Frage könnte in Kategorie 1 oder 3 fallen. Es ist seltsam, dass es die übergebene Liste sowohl verändert als auch zurückgibt; man sollte entweder das eine oder das andere wählen.

1 Stimmen

Die Diagnose lautet "etwas falsch machen". Das heißt, ich denke, es gibt Zeiten, in denen =None-Muster nützlich ist, aber im Allgemeinen wollen Sie nicht ändern, wenn ein mutable in diesem Fall übergeben (2). Die cache={} Muster ist wirklich eine reine Interview-Lösung, in echtem Code wollen Sie wahrscheinlich @lru_cache !

5 Stimmen

Das ist absolut nicht der Fall. In vielen Fällen ist es ein Designfehler und nicht der Programmierer, der etwas falsch macht.

0 Stimmen

@aCuria Sie haben also einen Fall 4, der sich von den 3 von mir vorgestellten unterscheidet? Ich würde gerne mehr darüber hören, bitte erzählen Sie mir mehr. Das Verhalten von Python mag in diesem Fall keinen Sinn machen, aber es ist an anderen Stellen sehr nützlich, und es zu ändern, wäre eine Katastrophe.

8voto

MisterMiyagi Punkte 35992

TLDR: Definitionszeitvorgaben sind konsistent und wesentlich aussagekräftiger.


Die Definition einer Funktion betrifft zwei Bereiche: den definierenden Bereich mit die Funktion und der Ausführungsbereich enthalten durch die Funktion. Es ist zwar ziemlich klar, wie Blöcke auf Bereiche abgebildet werden, aber die Frage ist, wo def <name>(<args=defaults>): gehört zu:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

El def name Teil debe im Definitionsbereich auswerten - wir wollen name zur Verfügung stehen. Würde die Funktion nur in sich selbst ausgewertet, wäre sie unzugänglich.

Desde parameter ein konstanter Name ist, können wir ihn gleichzeitig mit def name . Dies hat auch den Vorteil, dass die Funktion mit einer bekannten Signatur erzeugt wird als name(parameter=...): anstelle einer bloßen name(...): .

Nun, wann ist zu bewerten default ?

Die Konsistenz sagt bereits "bei der Definition": alles andere von def <name>(<args=defaults>): wird am besten auch bei der Definition bewertet. Eine Verzögerung von Teilen davon wäre die erstaunlichste Entscheidung.

Die beiden Möglichkeiten sind auch nicht gleichwertig: Wenn default zur Definitionszeit ausgewertet wird, ist es kann noch die Ausführungszeit beeinflussen. Wenn default zur Ausführungszeit ausgewertet wird, ist es no puede die Definitionszeit beeinflussen. Durch die Wahl von "bei der Definition" können beide Fälle ausgedrückt werden, während durch die Wahl von "bei der Ausführung" nur einer ausgedrückt werden kann:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

2 Stimmen

"Konsistenz" sagt bereits "bei der Definition": alles andere von def <name>(<args=defaults>): wird am besten auch bei der Definition bewertet." Ich glaube nicht, dass die Schlussfolgerung aus der Prämisse folgt. Nur weil zwei Dinge auf der gleichen Linie liegen, bedeutet das nicht, dass sie im gleichen Umfang bewertet werden sollten. default ist etwas anderes als der Rest der Zeile: Es ist ein Ausdruck. Die Auswertung eines Ausdrucks ist ein ganz anderer Prozess als die Definition einer Funktion.

1 Stimmen

@LarsH Funktionsdefinitionen sind sind in Python ausgewertet. Ob das von einer Anweisung ( def ) oder Ausdruck ( lambda ) ändert nichts daran, dass das Erstellen einer Funktion eine Auswertung bedeutet - insbesondere ihrer Signatur. Und Voreinstellungen sind Teil der Signatur einer Funktion. Das bedeutet nicht, dass die Vorgaben haben sofort ausgewertet werden, z.B. Typ-Hinweise nicht. Aber es legt nahe, dass sie es tun sollten, es sei denn, es gibt einen guten Grund, es nicht zu tun.

2 Stimmen

OK, das Erstellen einer Funktion bedeutet in gewissem Sinne eine Auswertung, aber natürlich nicht in dem Sinne, dass jeder Ausdruck darin zum Zeitpunkt der Definition ausgewertet wird. Die meisten sind es nicht. Es ist mir nicht klar, in welchem Sinne die Signatur zum Zeitpunkt der Definition besonders "ausgewertet" wird, genauso wenig wie der Funktionskörper "ausgewertet" (in eine geeignete Darstellung geparst) wird; während die Ausdrücke im Funktionskörper eindeutig nicht im vollen Sinne ausgewertet werden. Unter diesem Gesichtspunkt würde die Konsistenz besagen, dass Ausdrücke in der Signatur auch nicht "vollständig" ausgewertet werden sollten.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X