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.

42voto

Glenn Maynard Punkte 53282

Sie fragen, warum das so ist:

def func(a=[], b = 2):
    pass

ist intern nicht äquivalent zu diesem:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

außer im Fall des expliziten Aufrufs von func(None, None), den wir ignorieren werden.

Mit anderen Worten: Warum nicht jeden einzelnen Parameter speichern und beim Aufruf der Funktion auswerten, anstatt die Standardparameter auszuwerten?

Eine Antwort liegt wahrscheinlich genau dort - es würde jede Funktion mit Standardparametern in eine Schließung verwandeln. Selbst wenn das alles im Interpreter versteckt ist und keine vollständige Closure ist, müssen die Daten irgendwo gespeichert werden. Es wäre langsamer und würde mehr Speicher verbrauchen.

10 Stimmen

Es müsste keine Schließung sein - ein besserer Weg, um es zu denken, wäre einfach die Bytecode-Erstellung Vorgaben die erste Zeile des Codes zu machen - schließlich sind Sie den Körper an diesem Punkt sowieso kompilieren - es gibt keinen wirklichen Unterschied zwischen Code in den Argumenten und Code im Körper.

11 Stimmen

Stimmt, aber es würde Python immer noch verlangsamen, und es wäre eigentlich ziemlich überraschend, es sei denn, Sie tun das Gleiche für Klassendefinitionen, was es dumm langsam machen würde, da Sie die gesamte Klassendefinition jedes Mal neu ausführen müssten, wenn Sie eine Klasse instanziieren. Wie gesagt, die Lösung wäre überraschender als das Problem.

0 Stimmen

Ich stimme mit Lennart überein. Wie Guido zu sagen pflegt, gibt es für jede Sprachfunktion oder Standardbibliothek eine jemand da draußen, die es benutzen.

34voto

Ben Punkte 62082

Dies hat eigentlich nichts mit Standardwerten zu tun, außer dass es oft als unerwartetes Verhalten auftritt, wenn Sie Funktionen mit veränderbaren Standardwerten schreiben.

>>> def foo(a):
    a.append(5)
    print a

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

In diesem Code sind keine Standardwerte in Sicht, aber Sie haben genau das gleiche Problem.

Das Problem ist, dass foo es Ändern von eine veränderbare Variable, die vom Aufrufer übergeben wird, wenn der Aufrufer dies nicht erwartet. Code wie dieser wäre in Ordnung, wenn die Funktion wie folgt aufgerufen würde append_5 dann würde der Aufrufer die Funktion aufrufen, um den übergebenen Wert zu ändern, und dieses Verhalten wäre zu erwarten. Es ist jedoch sehr unwahrscheinlich, dass eine solche Funktion ein Standardargument annimmt, und wahrscheinlich würde sie die Liste nicht zurückgeben (da der Aufrufer bereits einen Verweis auf diese Liste hat, nämlich den, den er gerade übergeben hat).

Ihr Original foo mit einem Standardargument, sollte keine Änderungen an der a unabhängig davon, ob er explizit übergeben wurde oder den Standardwert erhielt. Ihr Code sollte veränderbare Argumente in Ruhe lassen, es sei denn, aus dem Kontext/Namen/der Dokumentation geht hervor, dass die Argumente verändert werden sollen. Die Verwendung von veränderbaren Werten, die als Argumente übergeben werden, als lokale Provisorien ist eine extrem schlechte Idee, unabhängig davon, ob es sich um Python handelt oder nicht und unabhängig davon, ob Standardargumente beteiligt sind oder nicht.

Wenn Sie ein lokales temporäres Element destruktiv manipulieren müssen, um etwas zu berechnen, und diese Manipulation mit einem Argumentwert beginnen müssen, müssen Sie eine Kopie erstellen.

10 Stimmen

Obwohl sie miteinander verbunden sind, handelt es sich meiner Meinung nach um ein anderes Verhalten (wie wir erwarten append zu ändern a "an Ort und Stelle"). Dass ein default mutable wird nicht bei jedem Aufruf neu instanziiert ist der "unerwartete" Teil... zumindest für mich :)

3 Stimmen

@AndyHayden wenn die Funktion erwartet um das Argument zu ändern, warum sollte es sinnvoll sein, einen Standard zu haben?

0 Stimmen

@MarkRansom das einzige Beispiel, das mir einfällt, ist cache={} . Ich vermute jedoch, dass dieses "geringste Erstaunen" auftritt, wenn Sie nicht erwarten (oder wollen), dass die Funktion, die Sie aufrufen, das Argument ändert.

33voto

Python: Das veränderliche Standardargument

Standardargumente werden zum Zeitpunkt der Kompilierung der Funktion in ein Funktionsobjekt ausgewertet. Wenn sie von der Funktion mehrfach verwendet werden, sind und bleiben sie das gleiche Objekt.

Wenn sie veränderbar sind, bleiben sie, wenn sie verändert werden (z. B. durch Hinzufügen eines Elements), bei aufeinanderfolgenden Aufrufen verändert.

Sie bleiben mutiert, weil sie jedes Mal das gleiche Objekt sind.

Äquivalenter Code:

Da die Liste bei der Kompilierung und Instanziierung des Funktionsobjekts an die Funktion gebunden wird, ist dies:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

entspricht fast genau dem hier:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Demonstration

Hier eine Demonstration - Sie können überprüfen, ob es sich jedes Mal um dasselbe Objekt handelt, wenn auf sie verwiesen wird durch

  • dass die Liste erstellt wird, bevor die Funktion zu einem Funktionsobjekt kompiliert wurde,
  • wobei zu beachten ist, dass die id bei jedem Verweis auf die Liste dieselbe ist,
  • wobei zu beachten ist, dass sich die Liste auch dann noch ändert, wenn die Funktion, die sie verwendet, ein zweites Mal aufgerufen wird,
  • Beachten Sie die Reihenfolge, in der die Ausgabe aus der Quelle gedruckt wird (die ich praktischerweise für Sie nummeriert habe):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))

if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

und führt es mit python example.py :

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Verstößt dies gegen den Grundsatz der "geringsten Verwunderung"?

Diese Reihenfolge der Ausführung ist für neue Benutzer von Python häufig verwirrend. Wenn Sie das Ausführungsmodell von Python verstehen, wird es ganz selbstverständlich.

Die übliche Anleitung für neue Python-Benutzer:

Aus diesem Grund werden neue Benutzer in der Regel angewiesen, ihre Standardargumente auf diese Weise zu erstellen:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Dies verwendet das Singleton None als Sentinel-Objekt, um der Funktion mitzuteilen, ob wir ein anderes Argument als das Standardargument erhalten haben oder nicht. Wenn wir kein Argument erhalten, dann wollen wir eigentlich eine neue leere Liste verwenden, [] als Standardeinstellung.

Da die Lehrgangsteil zum Kontrollfluss dice:

Wenn Sie nicht wollen, dass der Standardwert von nachfolgenden Aufrufen gemeinsam genutzt wird, können Sie die Funktion stattdessen wie folgt schreiben:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

29voto

Baczek Punkte 1049

Die kürzeste Antwort wäre wohl: "Definition ist Ausführung", daher macht das ganze Argument keinen strikten Sinn. Als etwas ausgeklügeltes Beispiel können Sie dies anführen:

def a(): return []

def b(x=a()):
    print x

Hoffentlich reicht das aus, um zu zeigen, dass die Nichtausführung der Standardargumente zum Zeitpunkt der Ausführung der def Aussage ist nicht einfach oder macht keinen Sinn, oder beides.

Ich stimme zu, dass es ein Problem ist, wenn Sie versuchen, Standardkonstruktoren zu verwenden.

29voto

Stéphane Punkte 1927

Das Thema hat mich bereits beschäftigt, aber nach dem, was ich hier gelesen habe, hat mir das Folgende geholfen zu erkennen, wie es intern funktioniert:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

4 Stimmen

Dies könnte für Neueinsteiger etwas verwirrend sein, da a = a + [1] Überlastungen a ... erwägen Sie, es zu ändern in b = a + [1] ; print id(b) und fügen Sie eine Zeile hinzu a.append(2) . Dadurch wird es deutlicher, dass + auf zwei Listen erzeugt immer eine neue Liste (zugeordnet zu b ), während eine modifizierte a können immer noch die gleichen id(a) .

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