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.

5voto

user2384994 Punkte 1659

Ich denke, die Antwort auf diese Frage liegt in der Art und Weise, wie Python Daten an Parameter übergibt (Übergabe durch Wert oder durch Verweis), nicht die Veränderbarkeit oder wie Python die "def"-Anweisung handhabt.

Eine kurze Einführung. Erstens gibt es zwei Arten von Datentypen in Python, einen einfachen elementaren Datentyp wie Zahlen und einen anderen Datentyp, nämlich Objekte. Zweitens: Bei der Übergabe von Daten an Parameter übergibt Python elementare Datentypen als Wert, d.h. es wird eine lokale Kopie des Wertes in einer lokalen Variablen erstellt, während Objekte als Referenz übergeben werden, d.h. als Zeiger auf das Objekt.

Wenn wir die beiden oben genannten Punkte anerkennen, können wir erklären, was mit dem Python-Code passiert ist. Es liegt nur an der Referenzübergabe für Objekte, hat aber nichts mit mutable/immutable oder der Tatsache zu tun, dass die "def"-Anweisung nur einmal ausgeführt wird, wenn sie definiert wird.

[] ist ein Objekt, also übergibt Python die Referenz von [] an a d.h., a ist nur ein Zeiger auf [], der als Objekt im Speicher liegt. Es gibt nur ein Exemplar von [], auf das jedoch viele Verweise bestehen. Für das erste foo() wird die Liste [] in 1 durch die Append-Methode. Beachten Sie jedoch, dass es nur eine Kopie des Listenobjekts gibt und dieses Objekt nun zu 1 . Wenn die zweite foo() ausgeführt wird, ist die Aussage auf der effbot-Webseite (items wird nicht mehr ausgewertet) falsch. a wird als das Listenobjekt ausgewertet, obwohl der Inhalt des Objekts jetzt 1 . Das ist der Effekt der Übergabe durch Referenz! Das Ergebnis von foo(3) lässt sich auf die gleiche Weise leicht ableiten.

Um meine Antwort zu untermauern, sehen wir uns zwei weitere Codes an.

\====== Nr. 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[] ein Objekt ist, ist auch None (Ersteres ist veränderbar, letzteres unveränderbar. Aber die Veränderlichkeit hat nichts mit der Frage zu tun). None ist irgendwo im Raum, aber wir wissen, dass es dort ist und dass es nur eine Kopie von None gibt. Jedes Mal, wenn foo aufgerufen wird, wird item ausgewertet (im Gegensatz zu einer Antwort, dass es nur einmal ausgewertet wird), um None zu sein, um genau zu sein, die Referenz (oder die Adresse) von None. In foo wird item dann in [] geändert, d.h. es zeigt auf ein anderes Objekt, das eine andere Adresse hat.

\====== Nr. 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

Der Aufruf von foo(1) führt dazu, dass die Elemente auf ein Listenobjekt [] mit einer Adresse von z. B. 11111111 zeigen. 1 in der Funktion foo in der Folge, aber die Adresse wird nicht geändert, immer noch 11111111. Dann kommt foo(2,[]). Obwohl das [] in foo(2,[]) den gleichen Inhalt hat wie der Standardparameter [] beim Aufruf von foo(1), sind ihre Adressen unterschiedlich! Da wir den Parameter explizit angeben, items muss die Adresse dieses neuen [] z. B. 2222222, und geben Sie es nach einer Änderung zurück. Jetzt wird foo(3) ausgeführt. da nur x angegeben wird, muss items wieder seinen Standardwert annehmen. Was ist der Standardwert? Er wird bei der Definition der Funktion foo festgelegt: das Listenobjekt mit der Adresse 11111111. Daher wird items als die Adresse 11111111 mit dem Element 1 ausgewertet. Die Liste mit der Adresse 2222222 enthält ebenfalls ein Element 2, auf das jedoch nicht mehr durch items verwiesen wird. Folglich macht ein Anhängen von 3 items [1,3].

Aus den obigen Ausführungen ergibt sich, dass die effbot Die in der akzeptierten Antwort empfohlene Webseite gab keine relevante Antwort auf diese Frage. Darüber hinaus halte ich einen Punkt auf der effbot-Webseite für falsch. Ich denke, dass der Code in Bezug auf den UI.Button korrekt ist:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

Jede Schaltfläche kann eine eigene Callback-Funktion enthalten, die einen anderen Wert von i . Ich kann dies an einem Beispiel verdeutlichen:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Wenn wir Folgendes ausführen x[7]() erhalten wir wie erwartet 7, und x[9]() ergibt 9, einen weiteren Wert von i .

6 Stimmen

Ihr letzter Punkt ist falsch. Versuchen Sie es und Sie werden sehen, dass x[7]() es 9 .

4 Stimmen

"python pass elementary data type by value, d.h. eine lokale Kopie des Wertes in eine lokale Variable" ist völlig falsch. Ich bin erstaunt, dass jemand offensichtlich Python sehr gut kennt und dennoch so ein schreckliches Missverständnis der Grundlagen hat :-(

-3voto

Charles Merriam Punkte 18484

Es gibt einen einfachen Weg zu verstehen, warum dies geschieht.

Python führt den Code von oben nach unten in einem Namespace aus.

Die "Interna" verkörpern lediglich diese Regel.

Der Grund für diese Wahl ist, "die Sprache in den Kopf zu bekommen". Alle seltsamen Eckfälle lassen sich durch die Ausführung von Code in einem Namespace vereinfachen: Standard-Immutables, verschachtelte Funktionen, Klassen (mit einem kleinen Patch-up nach dem Kompilieren), das self-Argument usw. In ähnlicher Weise könnte eine komplexe Syntax in einer einfachen Syntax geschrieben werden: a.foo(...) ist nur a.lookup('foo').__call__(a,...) . Dies funktioniert mit List Comprehensions, Dekoratoren, Metaklassen und mehr. Damit haben Sie einen nahezu perfekten Überblick über die seltsamen Ecken. Die Sprache passt in Ihren Kopf.

Sie sollten dranbleiben. Beim Erlernen von Python gibt es eine Phase, in der man sich über die Sprache aufregt, aber dann wird es bequem. Es ist die einzige Sprache, in der ich gearbeitet habe, die immer einfacher wird, je mehr man sich mit Eckfällen beschäftigt.

Hacken Sie weiter! Notizen behalten.

Für Ihren spezifischen Code, in zu vielen Details:

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

foo()

ist eine Aussage, die gleichbedeutend ist mit:

  1. Beginnen Sie mit der Erstellung eines Codeobjekts.
  2. Dolmetschen (a=[]) gerade jetzt, während wir gehen. Die [] ist der Standardwert des Arguments a. Es ist vom Typ Liste, da [] immer ist.
  3. Kompilieren Sie den gesamten Code nach der : in Python-Bytecode umwandeln und in eine andere Liste einfügen.
  4. Erstellen Sie das aufrufbare Wörterbuch mit den Argumenten und dem Code im Feld ' Code ' Feld
  5. Fügen Sie die aufrufbare Datei dem aktuellen Namespace im Feld "foo" hinzu.

Dann geht es in die nächste Zeile, foo() .

  1. Es ist kein reserviertes Wort, also im Namespace nachschlagen .
  2. Rufen Sie die Funktion auf, die die Liste als Standardargument verwenden wird. Beginnen Sie mit der Ausführung des Bytecodes in ihrem Namensraum.
  3. append wird keine neue Liste erstellt, sondern die alte Liste wird geändert.

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