Python-veränderbares Standardargument: Warum?

20

Ich weiß, dass Standardargumente zum Zeitpunkt der Funktionsinitialisierung und nicht bei jedem Funktionsaufruf erstellt werden. Siehe folgenden Code:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

Was ich nicht verstehe ist, warum dies so umgesetzt wurde. Welche Vorteile bietet dieses Verhalten gegenüber einer Initialisierung zu jeder Anrufzeit?

Sardathrion - Setzen Sie Monica wieder ein
quelle
Dies wird bereits in erstaunlichen Details zum Stack Overflow besprochen
Wildcard

Antworten:

14

Ich denke, der Grund dafür ist die einfache Implementierung. Lassen Sie mich näher darauf eingehen.

Der Standardwert der Funktion ist ein Ausdruck, den Sie auswerten müssen. In Ihrem Fall handelt es sich um einen einfachen Ausdruck, der nicht vom Abschluss abhängt, sondern freie Variablen enthalten kann def ook(item, lst = something.defaultList()). Wenn Sie Python entwerfen möchten, haben Sie die Wahl: Bewerten Sie es einmal, wenn die Funktion definiert wird, oder jedes Mal, wenn die Funktion aufgerufen wird. Python wählt das erste aus (im Gegensatz zu Ruby, das zur zweiten Option gehört).

Hierfür gibt es einige Vorteile.

Erstens erhalten Sie eine gewisse Geschwindigkeits- und Gedächtnissteigerung. In den meisten Fällen haben Sie unveränderliche Standardargumente, und Python kann sie nur einmal anstatt bei jedem Funktionsaufruf erstellen. Dies spart (etwas) Speicher und Zeit. Natürlich funktioniert es nicht sehr gut mit veränderlichen Werten, aber Sie wissen, wie Sie umgehen können.

Ein weiterer Vorteil ist die Einfachheit. Es ist recht einfach zu verstehen, wie der Ausdruck ausgewertet wird. Bei der Definition der Funktion wird der lexikalische Gültigkeitsbereich verwendet. Andernfalls kann sich der lexikalische Bereich zwischen der Definition und dem Aufruf ändern und das Debuggen etwas erschweren. Python ist in diesen Fällen sehr unkompliziert.

Stefan Kanev
quelle
3
Interessanter Punkt - obwohl es bei Python normalerweise das Prinzip der geringsten Überraschung ist. Einige Dinge sind in einem formalen Sinn der Komplexität des Modells einfach, aber nicht offensichtlich und überraschend, und ich denke, das zählt.
Steve314
1
Die geringste Überraschung dabei ist: Wenn Sie die Semantik "Bei jedem Aufruf auswerten" haben, können Sie eine Fehlermeldung erhalten, wenn sich der Abschluss zwischen zwei Funktionsaufrufen ändert (was durchaus möglich ist). Dies kann überraschender sein, als zu wissen, dass es einmal ausgewertet wird. Natürlich kann man behaupten, dass man, wenn man aus anderen Sprachen kommt, die Semantik bei jedem Aufruf außer Acht lässt und das ist die Überraschung, aber man kann sehen, wie es in beide Richtungen geht :)
Stefan Kanev
Guter Punkt zum Umfang
0xc0de
Ich denke, der Umfang ist tatsächlich das wichtigere Stück. Da Sie für den Standard nicht auf Konstanten beschränkt sind, benötigen Sie möglicherweise Variablen, die am Aufrufstandort nicht im Geltungsbereich sind.
Mark Ransom
5

Eine Möglichkeit , es zu setzen ist , dass das lst.append(item) nicht der Fall ist , die mutiert lstParameter. lstverweist immer noch auf die gleiche Liste. Es ist nur so, dass der Inhalt dieser Liste mutiert wurde.

Grundsätzlich hat Python überhaupt keine konstanten oder unveränderlichen Variablen, aber einige konstante, unveränderliche Typen. Sie können einen ganzzahligen Wert nicht ändern, sondern nur ersetzen. Sie können den Inhalt einer Liste jedoch ändern, ohne sie zu ersetzen.

Wie bei einer Ganzzahl können Sie eine Referenz nicht ändern, sondern nur ersetzen. Sie können jedoch den Inhalt des Objekts ändern, auf das verwiesen wird.

Was die einmalige Erstellung des Standardobjekts angeht, so ist dies meiner Meinung nach hauptsächlich eine Optimierung, um die Kosten für die Objekterstellung und die Garbage Collection zu senken.

Steve314
quelle
+1 Genau. Es ist wichtig, die Indirektionsebene zu verstehen - dass eine Variable kein Wert ist; stattdessen verweist es auf einen Wert. Um eine Variable zu ändern, kann der Wert ausgetauscht oder mutiert werden (falls es sich um eine veränderbare Variable handelt ).
Joonas Pulakka
Wenn ich mit kniffligen Variablen in Python konfrontiert werde, finde ich es hilfreich, "=" als "Name-Binding-Operator" zu betrachten. Der Name wird immer zurückgebunden, unabhängig davon, ob das Objekt, an das wir ihn binden, neu ist (neues Objekt oder unveränderliche Typinstanz) oder nicht.
StarWeaver
4

Welche Vorteile bietet dieses Verhalten gegenüber einer Initialisierung zu jeder Anrufzeit?

Hier können Sie das gewünschte Verhalten auswählen, wie Sie in Ihrem Beispiel gezeigt haben. Wenn Sie also möchten, dass das Standardargument unveränderlich ist, verwenden Sie einen unveränderlichen Wert wie Noneoder 1. Wenn Sie das Standardargument änderbar machen möchten, verwenden Sie etwas änderbares, z []. Es ist nur Flexibilität, wenn auch zugegebenermaßen, es kann beißen, wenn Sie es nicht wissen.

Joonas Pulakka
quelle
2

Ich denke, die eigentliche Antwort lautet: Python wurde als prozedurale Sprache geschrieben und erst nachträglich in funktionale Aspekte übernommen. Was Sie suchen, ist, dass die Standardeinstellung von Parametern als Abschluss ausgeführt wird, und dass Abschlüsse in Python wirklich nur halbherzig sind. Zum Beweis dieses Versuchs:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

Das gibt, [2, 2, 2]wo Sie eine echte Schließung erwarten würden, um zu produzieren [0, 1, 2].

Es gibt eine Menge Dinge, die ich gerne hätte, wenn Python die Möglichkeit hätte, Parameter, die standardmäßig in Closures enthalten sind, zu verpacken. Beispielsweise:

def foo(a, b=a.b):
    ...

Wäre sehr praktisch, aber "a" ist zur Zeit der Funktionsdefinition nicht im Gültigkeitsbereich, daher können Sie dies nicht tun und müssen stattdessen Folgendes tun:

def foo(a, b=None):
    if b is None:
        b = a.b

Welches ist fast das gleiche ... fast.

Ajscogo
quelle
1

Ein großer Vorteil ist das Auswendiglernen. Dies ist ein Standardbeispiel:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

und zum Vergleich:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Zeitmessungen in Ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s
steffen
quelle
0

Dies liegt daran, dass die Kompilierung in Python durch Ausführen des beschreibenden Codes ausgeführt wird.

Wenn einer sagte

def f(x = {}):
    ....

es wäre ziemlich klar, dass Sie jedes Mal ein neues Array wollten.

Aber was ist, wenn ich sage:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

Hier würde ich vermuten, dass ich Dinge in verschiedenen Listen erstellen und einen einzigen globalen Sammelbegriff haben möchte, wenn ich keine Liste spezifiziere.

Aber wie würde der Compiler das erraten? Warum also versuchen? Wir könnten uns darauf verlassen, ob dies benannt wurde oder nicht, und es könnte manchmal helfen, aber in Wirklichkeit würde es nur raten. Gleichzeitig gibt es einen guten Grund, es nicht zu versuchen - Konsistenz.

So wie es ist, führt Python nur den Code aus. Der Variablen list_of_all ist bereits ein Objekt zugewiesen, sodass dieses Objekt als Referenz in den Code übergeben wird, der standardmäßig x enthält, so wie ein Aufruf einer Funktion eine Referenz auf ein lokales Objekt erhalten würde, das hier genannt wird.

Wenn wir den unbenannten vom benannten Fall unterscheiden wollten, würde dies bedeuten, dass der Code bei der Kompilierung die Zuweisung auf eine wesentlich andere Weise ausführt als zur Laufzeit. Also machen wir den Sonderfall nicht.

Jon Jay Obermark
quelle
-5

Dies geschieht, weil Funktionen in Python erstklassige Objekte sind :

Default-Parameterwerte werden bei der Ausführung der Funktionsdefinition ausgewertet. Dies bedeutet, dass der Ausdruck einmalig ausgewertet wird , wenn die Funktion definiert wird, und dass für jeden Aufruf derselbe "vorberechnete" Wert verwendet wird .

Es wird weiter erläutert, dass durch das Bearbeiten des Parameterwerts der Standardwert für nachfolgende Aufrufe geändert wird und dass eine einfache Lösung mit der Verwendung von None als Standard mit einem expliziten Test im Funktionskörper alles ist, was erforderlich ist, um keine Überraschungen zu gewährleisten.

Dies bedeutet, def foo(l=[])dass diese Funktion beim Aufruf eine Instanz dieser Funktion wird und für weitere Aufrufe wiederverwendet wird. Stellen Sie sich Funktionsparameter so vor, als würden sie von den Attributen eines Objekts getrennt.

Profis könnten dies nutzen, um Klassen mit C-ähnlichen statischen Variablen zu haben. Deshalb ist es am besten, die Standardwerte None zu deklarieren und sie nach Bedarf zu initialisieren:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

ergibt:

[5] [5] [5] [5]

anstelle des Unerwarteten:

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]

umkehren
quelle
5
Nein. Sie können Funktionen (erstklassig oder nicht) unterschiedlich definieren, um den Standardargumentausdruck für jeden Aufruf erneut auszuwerten. Und alles, was danach folgt, dh ungefähr 90% der Antwort, steht völlig außer Frage. -1
1
Teilen Sie uns dieses Wissen über das Definieren von Funktionen zur Auswertung des Standardarguments für jeden Aufruf mit. Ich würde gerne einen einfacheren Weg kennenlernen, als in Python Docs empfohlen.
23.07.12
2
Auf einer Sprachdesignebene meine ich. Die Python-Sprachdefinition besagt derzeit, dass Standardargumente so behandelt werden, wie sie sind. Genauso gut könnte angegeben werden, dass Standardargumente auf andere Weise behandelt werden. IOW du antwortest auf die Frage, warum die Dinge so sind, wie sie sind.
Python hätte Standardparameter ähnlich wie Coffeescript implementieren können. Es würde Bytecode einfügen, um nach fehlenden Parametern zu suchen, und wenn sie fehlen, würde der Ausdruck ausgewertet.
Winston Ewert