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.

1917voto

rob Punkte 35132

Das ist kein Konstruktionsfehler, und es liegt auch nicht an den internen Komponenten oder der Leistung.
Das liegt einfach daran, dass Funktionen in Python Objekte erster Klasse sind und nicht nur ein Stück Code.

Wenn man sich das so vorstellt, dann macht es durchaus Sinn: eine Funktion ist ein Objekt, das nach seiner Definition ausgewertet wird; Standardparameter sind eine Art "Mitgliedsdaten" und können sich daher von einem Aufruf zum anderen ändern - genau wie bei jedem anderen Objekt.

Auf jeden Fall hat Effbot eine sehr schöne Erklärung für die 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.

0 Stimmen

Was meinen Sie mit "Objekten erster Klasse", Roberto? Ist es das, die "Notiz des Programmierers" direkt über den "Klassendefinitionen"? docs.python.org/reference/compound_stmts.html#class-definitions

0 Stimmen

Ich meine, dass es sich um "Objekte" handelt, die sich nicht wesentlich von dem unterscheiden, was man durch die Instanziierung einer Klasse erzeugt.

105 Stimmen

Allen, die die obige Antwort lesen, empfehle ich dringend, sich die Zeit zu nehmen, den verlinkten Effbot-Artikel durchzulesen. Neben all den anderen nützlichen Informationen ist der Teil darüber, wie diese Sprachfunktion für das Zwischenspeichern von Ergebnissen verwendet werden kann, sehr nützlich zu wissen!

336voto

Eli Courtwright Punkte 174547

Angenommen, Sie haben den folgenden Code

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

def eat(food=fruits):
    ...

Wenn ich die Deklaration von eat sehe, ist es am wenigsten verwunderlich zu denken, dass, wenn der erste Parameter nicht angegeben wird, er gleich dem Tupel ("apples", "bananas", "loganberries")

Nehmen wir jedoch an, dass ich später im Code etwas tue wie

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

wenn die Standardparameter bei der Funktionsausführung und nicht bei der Funktionsdeklaration gebunden würden, wäre ich (auf sehr unangenehme Weise) erstaunt, wenn ich feststellen würde, dass die Früchte geändert wurden. Das wäre IMO noch erstaunlicher als die Entdeckung, dass Ihre foo Die obige Funktion hat die Liste verändert.

Das eigentliche Problem liegt bei veränderlichen Variablen, und alle Sprachen haben dieses Problem bis zu einem gewissen Grad. Hier ist eine Frage: Nehmen wir an, ich habe in Java 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?

Verwendet meine Karte nun den Wert der StringBuffer Schlüssel, als er in die Karte eingefügt wurde, oder speichert er den Schlüssel per Verweis? Wie auch immer, jemand ist erstaunt; entweder die Person, die versucht hat, das Objekt aus der Map die einen Wert verwenden, der mit dem identisch ist, mit dem sie ihn hineingelegt haben, oder die Person, die ihr Objekt nicht abrufen kann, obwohl der Schlüssel, den sie verwenden, buchstäblich das gleiche Objekt ist, mit dem sie es in die Map eingefügt haben (das ist der Grund, warum Python nicht zulässt, dass seine eingebauten veränderlichen Datentypen als Wörterbuchschlüssel verwendet werden).

Ihr Beispiel ist ein gutes Beispiel für einen Fall, in dem Python-Neulinge überrascht und gebissen werden. Aber ich würde argumentieren, dass, wenn wir dies "beheben", dann würde das nur eine andere Situation schaffen, in der sie stattdessen gebissen werden, und diese wäre noch weniger intuitiv. Außerdem ist das immer der Fall, wenn man mit veränderlichen Variablen zu tun hat; man stößt immer auf Fälle, in denen jemand intuitiv das eine oder das andere Verhalten erwarten könnte, je nachdem, welchen Code er schreibt.

Mir persönlich gefällt der derzeitige Ansatz von Python: Standardfunktionsargumente werden bei der Definition der Funktion ausgewertet, und dieses Objekt ist immer der Standard. Ich nehme an, man könnte eine leere Liste als Sonderfall verwenden, aber diese Art von Sonderfall würde noch mehr Verwunderung hervorrufen, ganz zu schweigen davon, dass er nicht rückwärtskompatibel wäre.

51 Stimmen

Ich denke, das ist eine Frage der Debatte. Sie wirken auf eine globale Variable ein. Jede Auswertung, die irgendwo in Ihrem Code mit Ihrer globalen Variable durchgeführt wird, bezieht sich jetzt (korrekt) auf ("Blaubeeren", "Mangos"). der Standardparameter könnte wie jeder andere Fall sein.

71 Stimmen

Eigentlich glaube ich nicht, dass ich mit Ihrem ersten Beispiel einverstanden bin. Ich bin mir nicht sicher, ob mir die Idee gefällt, einen solchen Initialisierer überhaupt zu ändern, aber wenn ich es täte, würde ich erwarten, dass er sich genau so verhält, wie Sie es beschreiben - indem er den Standardwert in ("blueberries", "mangos") .

15 Stimmen

Der Standardparameter ist wie in jedem anderen Fall. Unerwartet ist, dass es sich bei dem Parameter um eine globale Variable und nicht um eine lokale Variable handelt. Das wiederum liegt daran, dass der Code bei der Funktionsdefinition und nicht beim Aufruf ausgeführt wird. Wenn man das verstanden hat und weiß, dass das Gleiche für Klassen gilt, ist es völlig klar.

315voto

glglgl Punkte 84928

Der entsprechende Teil der Dokumentation :

Standardparameterwerte werden bei der Ausführung der Funktionsdefinition von links nach rechts ausgewertet. Das bedeutet, dass der Ausdruck einmal ausgewertet wird, wenn die Funktion definiert wird, und dass derselbe "vorberechnete" Wert für jeden Aufruf verwendet wird. Dies ist besonders wichtig, wenn es sich bei einem Standardparameter um ein veränderliches Objekt handelt, wie z. B. eine Liste oder ein Wörterbuch: Wenn die Funktion das Objekt verändert (z. B. durch Anhängen eines Elements an eine Liste), wird der Standardwert tatsächlich verändert. Dies ist im Allgemeinen nicht beabsichtigt. Eine Möglichkeit, dies zu umgehen, ist die Verwendung von None als Standardeinstellung zu verwenden und im Hauptteil der Funktion explizit darauf zu testen, z.B.:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

291 Stimmen

Die Formulierungen "dies ist im Allgemeinen nicht das, was beabsichtigt war" und "es gibt eine Möglichkeit, dies zu umgehen" riechen so, als würden sie einen Konstruktionsfehler dokumentieren.

6 Stimmen

@Matthew: Ich bin mir dessen sehr wohl bewusst, aber es ist den Aufwand nicht wert. In der Regel werden Styleguides und Linters veränderbare Standardwerte aus diesem Grund bedingungslos als falsch kennzeichnen. Der explizite Weg, dasselbe zu tun, besteht darin, ein Attribut an die Funktion anzuhängen ( function.data = [] ) oder noch besser, ein Objekt erstellen.

16 Stimmen

@bukzor: Fallstricke müssen bemerkt und dokumentiert werden, weshalb diese Frage gut ist und so viele positive Bewertungen erhalten hat. Gleichzeitig müssen Fallstricke nicht unbedingt beseitigt werden. Wie viele Python-Anfänger haben schon eine Liste an eine Funktion übergeben, die sie verändert hat, und waren schockiert, als die Änderungen in der ursprünglichen Variablen auftauchten? Dabei sind veränderbare Objekttypen wunderbar, wenn man weiß, wie sie zu verwenden sind. Ich schätze, dass es bei diesem speziellen Fallstrick einfach auf die Meinung ankommt.

148voto

Utaal Punkte 8264

Ich weiß nichts über das Innenleben des Python-Interpreters (und ich bin auch kein Experte für Compiler und Interpreter), also beschuldigen Sie mich nicht, wenn ich etwas Unsinniges oder Unmögliches vorschlage.

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

a = []

Sie erwarten, dass Sie eine neu Liste referenziert von a .

Warum sollte die a=[] in

def x(a=[]):

eine neue Liste bei der Funktionsdefinition und nicht beim Aufruf instanziieren? Das ist so, als ob Sie fragen würden: "Wenn der Benutzer das Argument nicht angibt, dann instanziieren eine neue Liste erstellen und sie so verwenden, als ob sie vom Aufrufer erstellt worden wäre". Ich denke, dies ist eher zweideutig:

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

Benutzer, wollen Sie a auf den Zeitpunkt zu setzen, der dem Zeitpunkt der Definition oder Ausführung der x ? In diesem Fall, wie im vorherigen, werde ich das gleiche Verhalten beibehalten, als ob das Standardargument "Zuweisung" die erste Anweisung der Funktion wäre ( datetime.now() beim Funktionsaufruf aufgerufen). Andererseits könnte der Benutzer, wenn er die Zuordnung zur Definitionszeit wünscht, schreiben:

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

Ich weiß, ich weiß: Das ist ein Schlussstrich. Alternativ könnte Python ein Schlüsselwort bereitstellen, um die Bindung zur Definitionszeit zu erzwingen:

def x(static a=b):

14 Stimmen

Sie könnten Folgendes tun: def x(a=None): Und dann, wenn a=None ist, a=datetime.datetime.now() setzen

35 Stimmen

Ich danke Ihnen dafür. Ich konnte nicht wirklich herausfinden, warum mich das so sehr ärgert. Sie haben es wunderbar gemacht, mit einem Minimum an Unschärfe und Verwirrung. Als jemand, der aus der Systemprogrammierung in C++ kommt und manchmal naiv Sprachfeatures "übersetzt", hat mich dieser falsche Freund ganz schön vor den Kopf gestoßen, genau wie die Klassenattribute. Ich verstehe, warum die Dinge so sind, aber ich kann nicht anders, als es zu verabscheuen, ganz gleich, welche positiven Auswirkungen es haben mag. Zumindest ist es so konträr zu meinen Erfahrungen, dass ich es wahrscheinlich (hoffentlich) nie vergessen werde...

6 Stimmen

@Andreas wenn Sie Python lange genug benutzen, fangen Sie an zu sehen, wie logisch es für Python ist, Dinge als Klassenattribute so zu interpretieren, wie es das tut - es ist nur wegen der besonderen Eigenheiten und Beschränkungen von Sprachen wie C++ (und Java, und C#...), dass es für Inhalte der class {} Block, der als zugehörig zum Instanzen :) Aber wenn Klassen Objekte erster Klasse sind, ist es natürlich, dass ihr Inhalt (im Speicher) ihren Inhalt (im Code) widerspiegelt.

99voto

Lennart Regebro Punkte 157632

Nun, der Grund dafür ist ganz einfach, dass Bindungen bei der Ausführung von Code ausgeführt werden, und die Funktionsdefinition wird ausgeführt, nun ja... wenn die Funktionen definiert sind.

Vergleichen Sie dies:

class BananaBunch:
    bananas = []

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

Dieser Code leidet unter genau demselben unerwarteten Umstand. bananas ist ein Klassenattribut und wird daher allen Instanzen dieser Klasse hinzugefügt, wenn Sie ihr etwas hinzufügen. Der Grund dafür ist genau derselbe.

Es ist einfach "wie es funktioniert", und es anders zu machen im Fall der Funktion wäre wahrscheinlich kompliziert, und im Fall der Klasse wahrscheinlich unmöglich, oder zumindest verlangsamen Objekt Instanziierung viel, wie Sie die Klasse Code um zu halten und führen Sie es, wenn Objekte erstellt werden müssen.

Ja, das ist unerwartet. Aber wenn der Groschen erst einmal gefallen ist, passt es perfekt dazu, wie Python im Allgemeinen funktioniert. In der Tat, es ist eine gute Lehrhilfe, und wenn Sie verstehen, warum dies geschieht, werden Sie grok Python viel besser.

Dennoch sollte es in jedem guten Python-Tutorial eine wichtige Rolle spielen. Denn wie Sie erwähnen, jeder läuft in dieses Problem früher oder später.

0 Stimmen

Wie kann man ein Klassenattribut definieren, das für jede Instanz einer Klasse unterschiedlich ist?

21 Stimmen

Wenn es für jede Instanz anders ist, handelt es sich nicht um ein Klassenattribut. Klassenattribute sind Attribute der Klasse CLASS. Daher auch der Name. Daher sind sie für alle Instanzen gleich.

2 Stimmen

Wie definiert man ein Attribut in einer Klasse, das für jede Instanz einer Klasse anders ist? (Neu definiert für diejenigen, die nicht erkennen konnten, dass eine Person, die mit den Namenskonventionen von Python nicht vertraut ist, nach normalen Mitgliedsvariablen einer Klasse fragen könnte).

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