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.

28voto

Jason Baker Punkte 180981

Es ist eine Leistungsoptimierung. Welche der beiden Funktionsaufrufe ist Ihrer Meinung nach aufgrund dieser Funktionalität schneller?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Ich gebe Ihnen einen Tipp. Hier ist die Demontage (siehe http://docs.python.org/library/dis.html ):

# 1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

# 2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Ich bezweifle, dass das erfahrene Verhalten einen praktischen Nutzen hat (wer hat wirklich statische Variablen in C verwendet, ohne Bugs zu züchten?)

Wie Sie sehen können, gibt es es einen Leistungsvorteil bei der Verwendung unveränderlicher Standardargumente. Dies kann einen Unterschied machen, wenn es sich um eine häufig aufgerufene Funktion handelt oder das Standardargument lange Zeit zum Aufbau benötigt. Bedenken Sie auch, dass Python nicht C ist. In C haben Sie Konstanten, die so gut wie frei sind. In Python haben Sie diesen Vorteil nicht.

25voto

Dmitry Minkovsky Punkte 33223

Dieses Verhalten ist nicht überraschend, wenn man Folgendes bedenkt:

  1. Das Verhalten von schreibgeschützten Klassenattributen bei Zuweisungsversuchen, und dass
  2. Funktionen sind Objekte (in der akzeptierten Antwort gut erklärt).

Die Rolle der (2) wurde in diesem Thread bereits ausführlich behandelt. (1) ist wahrscheinlich der Faktor, der Erstaunen hervorruft, da dieses Verhalten nicht "intuitiv" ist, wenn man aus anderen Sprachen kommt.

(1) wird in der Python-Schrift beschrieben Tutorium über Klassen . Bei dem Versuch, einem schreibgeschützten Klassenattribut einen Wert zuzuweisen:

...alle Variablen, die außerhalb des innersten Bereichs gefunden werden, sind schreibgeschützt ( Der Versuch, in eine solche Variable zu schreiben, erzeugt lediglich ein neue lokale Variable im innersten Bereich, wobei die gleichnamige benannte äußere Variable unverändert ).

Gehen Sie noch einmal auf das ursprüngliche Beispiel zurück und berücksichtigen Sie die oben genannten Punkte:

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

Aquí foo ist ein Objekt und a ist ein Attribut von foo (verfügbar unter foo.func_defs[0] ). Da a ist eine Liste, a ist veränderbar und daher ein Lese-/Schreib-Attribut von foo . Sie wird bei der Instanziierung der Funktion auf die in der Signatur angegebene leere Liste initialisiert und ist zum Lesen und Schreiben verfügbar, solange das Funktionsobjekt existiert.

Aufruf von foo ohne eine Voreinstellung zu überschreiben, wird der Wert dieser Voreinstellung aus foo.func_defs . In diesem Fall, foo.func_defs[0] wird verwendet für a innerhalb des Codebereichs des Funktionsobjekts. Änderungen an a ändern foo.func_defs[0] die Teil des foo Objekt und bleibt zwischen der Ausführung des Codes in foo .

Vergleichen Sie dies nun mit dem Beispiel aus der Dokumentation zu Nachahmung des Standardargumentverhaltens anderer Sprachen so dass die Funktionssignaturvorgaben bei jeder Ausführung der Funktion verwendet werden:

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

Unter (1) y (2) berücksichtigt, kann man sehen, warum dies das gewünschte Verhalten erreicht:

  • Wenn die foo Funktionsobjekt instanziiert wird, foo.func_defs[0] wird eingestellt auf None ein unveränderliches Objekt.
  • Wenn die Funktion mit Standardwerten ausgeführt wird (ohne Angabe eines Parameters für L im Funktionsaufruf), foo.func_defs[0] ( None ) ist im lokalen Bereich verfügbar als L .
  • Unter L = [] kann die Zuordnung nicht gelingen bei foo.func_defs[0] weil dieses Attribut schreibgeschützt ist.
  • Per (1) , eine neue lokale Variable, ebenfalls mit dem Namen L wird im lokalen Bereich erstellt und für den Rest des Funktionsaufrufs verwendet. foo.func_defs[0] bleibt somit bei künftigen Aufrufen von foo .

22voto

hugo24 Punkte 1041

Eine einfache Umgehung mit None

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

0 Stimmen

Das ist keine Antwort auf die Frage.

22voto

Christos Hayward Punkte 5489

Das mag wahr sein:

  1. Jemand benutzt jede Sprache/Bibliotheksfunktion, und
  2. Es wäre nicht ratsam, das Verhalten hier zu ändern, aber

es ist durchaus konsequent, an beiden oben genannten Merkmalen festzuhalten und dennoch einen weiteren Punkt zu machen:

  1. Das ist eine verwirrende Funktion und in Python unglücklich.

Die anderen Antworten, oder zumindest einige von ihnen, beziehen sich entweder auf die Punkte 1 und 2, aber nicht auf 3, oder sie beziehen sich auf Punkt 3 und spielen die Punkte 1 und 2 herunter. Aber alle drei sind wahr.

Es mag wahr sein, dass ein Pferdewechsel in der Mitte des Weges hier zu einem erheblichen Bruch führen würde, und dass es noch mehr Probleme geben könnte, wenn man Python so ändert, dass es intuitiv mit Stefanos Anfangsschnipsel umgehen kann. Und es mag wahr sein, dass jemand, der die Interna von Python gut kennt, ein Minenfeld von Konsequenzen erklären könnte. Allerdings,

Das bestehende Verhalten ist nicht pythonisch, und Python ist erfolgreich, weil nur sehr wenig an der Sprache gegen das Prinzip des geringsten Erstaunens verstößt in der Nähe von so schlecht. Es ist ein echtes Problem, unabhängig davon, ob es klug wäre, es auszurotten oder nicht. Es ist ein Konstruktionsfehler. Wenn man die Sprache besser versteht, indem man versucht, das Verhalten nachzuvollziehen, kann ich sagen, dass C++ all dies und noch mehr tut; man lernt viel, wenn man zum Beispiel subtile Zeigerfehler entdeckt. Aber das ist nicht pythonisch: Leute, die sich genug für Python interessieren, um angesichts dieses Verhaltens durchzuhalten, sind Leute, die von der Sprache angezogen werden, weil Python viel weniger Überraschungen bietet als andere Sprachen. Dabblers und Neugierige werden zu Pythonistas, wenn sie erstaunt sind, wie wenig Zeit es braucht, um etwas zum Laufen zu bringen - nicht wegen eines Designfl - ich meine, eines versteckten Logikrätsels -, das gegen die Intuition von Programmierern verstößt, die von Python angezogen werden, weil es Funktioniert einfach .

7 Stimmen

-1 Dies ist zwar eine vertretbare Perspektive, aber keine Antwort, y Ich bin damit nicht einverstanden. Zu viele Ausnahmeregelungen bringen ihre eigenen Eckfälle hervor.

6 Stimmen

Dann ist es also "erstaunlich ignorant" zu sagen, dass es in Python sinnvoller wäre, wenn ein Standardargument von [] jedes Mal, wenn die Funktion aufgerufen wird, [] bleibt?

4 Stimmen

Und ist es ignorant, ein Standardargument als unglückliche Redewendung zu betrachten, indem man es auf "Kein" setzt, und dann im Hauptteil der Funktion zu sagen, wenn argument == Kein: argument = []? Ist es ignorant, dieses Idiom für unglücklich zu halten, weil die Leute oft das wollen, was ein naiver Neuling erwarten würde, nämlich dass, wenn man f(argument = []) zuweist, argument automatisch den Wert [] annimmt?

20voto

Alexander Punkte 96032

Ich werde eine alternative Struktur demonstrieren, um einen Standardlistenwert an eine Funktion zu übergeben (es funktioniert genauso gut mit Wörterbüchern).

Wie von anderen bereits ausführlich kommentiert, wird der Listenparameter an die Funktion gebunden, wenn sie definiert wird, und nicht erst, wenn sie ausgeführt wird. Da Listen und Wörterbücher veränderbar sind, wirkt sich jede Änderung an diesem Parameter auf andere Aufrufe dieser Funktion aus. Infolgedessen erhalten nachfolgende Funktionsaufrufe diese gemeinsame Liste, die möglicherweise durch andere Funktionsaufrufe geändert wurde. Noch schlimmer ist, dass zwei Parameter gleichzeitig den gemeinsamen Parameter dieser Funktion verwenden, ohne zu wissen, welche Änderungen der andere vorgenommen hat.

Falsche Methode (wahrscheinlich...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Sie können überprüfen, ob es sich um ein und dasselbe Objekt handelt, indem Sie id :

>>> id(a)
5347866528

>>> id(b)
5347866528

In Brett Slatkins Buch "Effektives Python: 59 spezifische Wege, um besseres Python zu schreiben", Punkt 20: Verwendung None und Docstrings zur Angabe dynamischer Standardargumente (p. 48)

Die Konvention, um das gewünschte Ergebnis in Python zu erreichen, lautet einen Standardwert von None und um das tatsächliche Verhalten zu dokumentieren im Docstring zu dokumentieren.

Diese Implementierung stellt sicher, dass jeder Aufruf der Funktion entweder die Standardliste oder die an die Funktion übergebene Liste erhält.

Bevorzugte Methode :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

Es kann legitime Anwendungsfälle für die "Falsche Methode" geben, in denen der Programmierer beabsichtigt, den Standardlistenparameter gemeinsam zu nutzen, aber dies ist eher die Ausnahme als die Regel.

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