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.

89voto

Warum schaust du nicht nach innen?

Ich bin wirklich überraschenderweise hat niemand die aufschlussreiche Introspektion durchgeführt, die Python bietet ( 2 y 3 gelten) auf abrufbare Forderungen.

Gegeben eine einfache kleine Funktion func definiert als:

>>> def func(a = []):
...    a.append(5)

Wenn Python auf diese Datei stößt, wird sie zunächst kompiliert, um eine code Objekt für diese Funktion. Während dieser Kompilierungsschritt durchgeführt wird, Python wertet aus. * und dann speichert die Standardargumente (eine leere Liste [] hier) im Funktionsobjekt selbst . Wie in der ersten Antwort erwähnt: die Liste a kann nun als ein Mitglied der Funktion func .

Machen wir also eine Selbstbeobachtung, ein Vorher und Nachher, um zu untersuchen, wie die Liste erweitert wird innerhalb das Funktionsobjekt. Ich verwende Python 3.x dafür, für Python 2 gilt das Gleiche (verwenden Sie __defaults__ o func_defaults in Python 2; ja, zwei Namen für dieselbe Sache).

Funktion vor der Ausführung:

>>> def func(a = []):
...     a.append(5)
...     

Nachdem Python diese Definition ausgeführt hat, übernimmt es alle angegebenen Standardparameter ( a = [] hier) und stopfen sie in den __defaults__ Attribut für das Funktionsobjekt (relevanter Abschnitt: Abrufbare Forderungen):

>>> func.__defaults__
([],)

O.k., also eine leere Liste als einziger Eintrag in __defaults__ genau wie erwartet.

Funktion nach der Ausführung:

Führen wir nun diese Funktion aus:

>>> func()

Nun, sehen wir uns diese __defaults__ wieder:

>>> func.__defaults__
([5],)

Erstaunt? Der Wert innerhalb des Objekts ändert sich! Aufeinanderfolgende Aufrufe der Funktion werden nun einfach an den eingebetteten Wert angehängt list Objekt:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Da haben Sie es also, den Grund, warum diese Makel geschieht, liegt daran, dass Standardargumente Teil des Funktionsobjekts sind. Hier geht nichts Seltsames vor, es ist nur alles ein bisschen überraschend.

Die übliche Lösung zur Bekämpfung dieses Problems ist die Verwendung von None als Standardwert festlegen und dann im Funktionsrumpf initialisieren:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Da der Funktionskörper jedes Mal neu ausgeführt wird, erhalten Sie immer eine neue, leere Liste, wenn kein Argument für a .


Um weiter zu überprüfen, ob die Liste in __defaults__ ist die gleiche wie die in der Funktion func können Sie Ihre Funktion einfach so ändern, dass sie die id der Liste a innerhalb des Funktionskörpers verwendet. Vergleichen Sie sie dann mit der Liste in __defaults__ (Stellung [0] en __defaults__ ) und Sie werden sehen, dass diese sich tatsächlich auf dieselbe Listeninstanz beziehen:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Und das alles mit der Kraft der Introspektion!


* Um zu überprüfen, ob Python die Standardargumente während der Kompilierung der Funktion auswertet, versuchen Sie Folgendes auszuführen:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

wie Sie feststellen werden, input() wird aufgerufen, bevor die Funktion erstellt und an den Namen gebunden wird bar gemacht wird.

1 Stimmen

Ist id(...) für diese letzte Überprüfung benötigt, oder würde die is Betreiber die gleiche Frage beantworten?

0 Stimmen

Verwendung von None als Standardeinstellung schränkt die Nützlichkeit der __defaults__ Selbstbeobachtung, also glaube ich nicht, dass das eine gute Verteidigung dafür ist, dass man __defaults__ wie es funktioniert. Lazy-Evaluation würde mehr dazu beitragen, dass die Funktionsvorgaben für beide Seiten nützlich bleiben.

71voto

Brian Punkte 112487

Ich war früher der Meinung, dass die Erstellung der Objekte zur Laufzeit der bessere Ansatz wäre. Jetzt bin ich mir da nicht mehr so sicher, da man einige nützliche Funktionen verliert, aber es könnte sich trotzdem lohnen, einfach um Verwirrung bei Neulingen zu vermeiden. Die Nachteile dieses Ansatzes sind:

1. Leistung

def foo(arg=something_expensive_to_compute())):
    ...

Wenn die Auswertung zur Aufrufzeit erfolgt, wird die teure Funktion jedes Mal aufgerufen, wenn Ihre Funktion ohne ein Argument verwendet wird. Entweder zahlen Sie bei jedem Aufruf einen hohen Preis, oder Sie müssen den Wert manuell extern zwischenspeichern, was Ihren Namespace verschmutzt und ihn unübersichtlich macht.

2. Erzwingen von gebundenen Parametern

Ein nützlicher Trick besteht darin, die Parameter eines Lambdas an die aktuell Bindung einer Variablen bei der Erstellung des Lambdas. Zum Beispiel:

funcs = [ lambda i=i: i for i in range(10)]

Dies gibt eine Liste von Funktionen zurück, die jeweils 0,1,2,3... zurückgeben. Wenn das Verhalten geändert wird, binden sie stattdessen i a la Anrufzeit Wert von i, so dass Sie eine Liste von Funktionen erhalten würden, die alle Folgendes zurückgeben 9 .

Die einzige Möglichkeit, dies anders zu realisieren, wäre, einen weiteren Abschluss mit der i-Bindung zu erstellen, d. h.:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Introspektion

Betrachten Sie den Code:

def foo(a='test', b=100, c=[]):
   print a,b,c

Wir können Informationen über die Argumente und Voreinstellungen mit der inspect Modul, das

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Diese Informationen sind sehr nützlich für Dinge wie Dokumentenerstellung, Metaprogrammierung, Dekoratoren usw.

Nehmen wir nun an, das Verhalten der Standardwerte könnte so geändert werden, dass dies dem entspricht:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

Wir haben jedoch die Fähigkeit verloren, uns selbst zu prüfen und zu sehen, was die Standardargumente sind . Da die Objekte noch nicht konstruiert wurden, können wir nicht auf sie zugreifen, ohne die Funktion aufzurufen. Das Beste, was wir tun können, ist, den Quellcode zu speichern und ihn als String zurückzugeben.

2 Stimmen

Sie könnten die Introspektion auch erreichen, wenn es für jedes eine Funktion gäbe, die das Standardargument anstelle eines Wertes erzeugt. Das Inspektionsmodul ruft diese Funktion einfach auf.

0 Stimmen

@SilentGhost: Ich spreche davon, ob das Verhalten geändert wurde, um es neu zu erstellen - es einmal zu erstellen ist das aktuelle Verhalten und der Grund, warum das Problem der veränderlichen Voreinstellung besteht.

3 Stimmen

@yairchu: Das setzt voraus, dass die Konstruktion sicher ist (d.h. keine Seiteneffekte hat). Introspecting die Args sollte nicht tun irgendetwas, aber die Bewertung eines beliebigen Codes könnte sich durchaus auswirken.

65voto

Lutz Prechelt Punkte 32254

5 Punkte zur Verteidigung von Python

  1. Vereinfachung : Das Verhalten ist im folgenden Sinne einfach: Die meisten Menschen tappen nur einmal in diese Falle, nicht mehrmals.

  2. Konsistenz : Python immer übergibt Objekte, keine Namen. Der Standardparameter ist natürlich Teil der Funktion Überschrift (nicht des Funktionskörpers). Er sollte daher ausgewertet werden zum Zeitpunkt des Ladens des Moduls (und nur zum Zeitpunkt des Ladens des Moduls, es sei denn, er ist verschachtelt) und nicht zur Zeit des Funktionsaufrufs.

  3. Nützlichkeit : Wie Frederik Lundh in seiner Erklärung von "Standard-Parameterwerte in Python" die Das aktuelle Verhalten kann für die fortgeschrittene Programmierung recht nützlich sein. (Sparsam verwenden.)

  4. Ausreichende Dokumentation : In der grundlegendsten Python-Dokumentation, dem Tutorial, wird das Problem lautstark angekündigt als ein "Wichtige Warnung" において erste Unterabschnitt des Abschnitts "Mehr über die Definition von Funktionen" . In der Warnung wird sogar Fettschrift verwendet, was außerhalb von Überschriften selten verwendet wird. RTFM: Lesen Sie das ausführliche Handbuch.

  5. Meta-Lernen : In die Falle zu tappen ist eigentlich ein sehr ein sehr hilfreicher Moment (zumindest wenn Sie ein reflektierender Lernender sind), weil man dann den Punkt besser versteht "Konsistenz" besser verstehen, und das wird Sie viel über Python lernen.

24 Stimmen

Ich habe ein Jahr gebraucht, um herauszufinden, dass dieses Verhalten meinen Code in der Produktion durcheinander bringt, und habe schließlich eine komplette Funktion entfernt, bis ich zufällig auf diesen Designfehler gestoßen bin. Ich verwende Django. Da es in der Staging-Umgebung nicht viele Anfragen gab, hatte dieser Fehler nie Auswirkungen auf die Qualitätssicherung. Als wir live gingen und viele gleichzeitige Anfragen erhielten, begannen einige Funktionen, die Parameter der anderen zu überschreiben! Das führte zu Sicherheitslücken, Bugs und was nicht alles.

22 Stimmen

@oriadam, nichts für ungut, aber ich frage mich, wie Sie Python gelernt haben, ohne vorher auf dieses Problem zu stoßen. Ich lerne gerade Python und dieser mögliche Fallstrick ist in der offiziellen Python-Anleitung erwähnt gleich bei der ersten Erwähnung von Standardargumenten. (Wie in Punkt 4 dieser Antwort erwähnt.) Ich nehme an, die Moral ist - ziemlich unsympathisch - die offizielle Dokumente der Sprache, die Sie für die Erstellung von Produktionssoftware verwenden.

2 Stimmen

Außerdem wäre es (für mich) überraschend, wenn zusätzlich zu dem Funktionsaufruf, den ich tätige, eine Funktion von unbekannter Komplexität aufgerufen würde.

58voto

ymv Punkte 2067

Dieses Verhalten lässt sich leicht erklären:

  1. die Deklaration der Funktion (Klasse usw.) wird nur einmal ausgeführt, wobei alle Standardwertobjekte erzeugt werden
  2. alles wird als Referenz übergeben

Also:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a ändert sich nicht - jeder Zuweisungsaufruf erzeugt ein neues int-Objekt - neues Objekt wird gedruckt
  2. b ändert sich nicht - das neue Array wird aus dem Standardwert erstellt und gedruckt
  3. c Änderungen - die Operation wird am selben Objekt durchgeführt - und es wird gedruckt

0 Stimmen

(Eigentlich, hinzufügen. ist ein schlechtes Beispiel, aber dass ganze Zahlen unveränderlich sind, ist immer noch mein Hauptargument).

0 Stimmen

Realisiert es zu meinem Leidwesen nach der Überprüfung zu sehen, dass, mit b auf [], b.__add__([1]) gibt [1] aber auch lässt b noch [] obwohl Listen veränderbar sind. Mein Fehler.

0 Stimmen

@ANon: Es gibt __iadd__ , aber es funktioniert nicht mit int. Natürlich. :-)

43voto

hynekcer Punkte 13689

1) Das so genannte Problem des "Mutable Default Argument" ist im Allgemeinen ein spezielles Beispiel, das dies zeigt:
"Alle Funktionen mit diesem Problem leiden auch unter einem ähnlichen Nebenwirkungsproblem beim aktuellen Parameter ,"
Das verstößt gegen die Regeln der funktionalen Programmierung, ist in der Regel unerwünscht und sollte gemeinsam behoben werden.

Beispiel:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Lösung : a kopieren.
Eine absolut sichere Lösung ist copy o deepcopy zuerst das Eingabeobjekt und dann die Kopie zu verwenden.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Viele eingebaute veränderliche Typen haben eine Kopiermethode wie some_dict.copy() o some_set.copy() oder kann einfach kopiert werden wie somelist[:] o list(some_list) . Jedes Objekt kann auch kopiert werden durch copy.copy(any_object) oder gründlicher durch copy.deepcopy() (letzteres ist nützlich, wenn das veränderbare Objekt aus veränderbaren Objekten zusammengesetzt ist). Einige Objekte beruhen grundsätzlich auf Seiteneffekten wie das Objekt "Datei" und können nicht sinnvoll durch Kopieren reproduziert werden. Kopieren von

Beispielproblem für eine ähnliche SO-Frage

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

Es sollte auch nicht in irgendeiner Datenbank gespeichert werden. öffentlich Attribut einer von dieser Funktion zurückgegebenen Instanz. (Unter der Annahme, dass privat Attribute von Instanzen sollten nicht von außerhalb dieser Klasse oder Unterklassen geändert werden, d.h. _var1 ist ein privates Attribut )

Schlussfolgerung:
Objekte mit Eingabeparametern sollten weder an Ort und Stelle verändert (mutiert) noch in ein von der Funktion zurückgegebenes Objekt eingebunden werden. (Wenn wir eine Programmierung ohne Seiteneffekte bevorzugen, was dringend empfohlen wird, siehe Wiki über "Nebenwirkung" (Die ersten beiden Absätze sind in diesem Zusammenhang von Bedeutung.) .)

2)
Nur wenn die Nebenwirkung auf den aktuellen Parameter erforderlich, auf den Standardparameter jedoch unerwünscht ist, ist die folgende Lösung sinnvoll def ...(var1=None): if var1 is None: var1 = [] Mehr

3) In einigen Fällen ist das veränderliche Verhalten von Standardparametern nützlich .

7 Stimmen

Ich hoffe, Sie sind sich bewusst, dass Python ノット eine funktionale Programmiersprache.

11 Stimmen

Ja, Python ist eine multiparagigmatische Sprache mit einigen funktionalen Eigenschaften. ("Lass nicht jedes Problem wie einen Nagel aussehen, nur weil du einen Hammer hast.") Viele davon sind in den Best Practices von Python enthalten. Python hat eine interessante HOWTO Funktionale Programmierung Weitere Merkmale sind Verschlüsse und Currying, die hier nicht erwähnt werden.

4 Stimmen

Zu diesem späten Zeitpunkt möchte ich noch hinzufügen, dass die Zuweisungssemantik von Python explizit darauf ausgelegt ist, das Kopieren von Daten zu vermeiden, wo es notwendig ist. Die Erstellung von Kopien (und insbesondere von tiefen Kopien) wirkt sich daher sowohl auf die Laufzeit als auch auf die Speichernutzung negativ aus. Sie sollten daher nur verwendet werden, wenn es notwendig ist, aber Neulinge haben oft Schwierigkeiten zu verstehen, wann das der Fall ist.

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