"Am wenigsten Erstaunen" und das veränderbare Standardargument


2594

Jeder, der lange genug an Python bastelt, wurde von folgendem 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 stattdessen sehr unterschiedlich und sehr erstaunlich (für einen Anfänger):

>>> 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 antwortete, dass das Verhalten eine zugrunde liegende Erklärung habe, und es ist in der Tat sehr rätselhaft und unerwartet, wenn Sie die Interna nicht verstehen. Ich konnte jedoch die folgende Frage (für mich selbst) nicht beantworten: Was ist der Grund für die Bindung des Standardarguments bei der Funktionsdefinition und nicht bei der Funktionsausführung? Ich bezweifle, dass das erlebte Verhalten einen praktischen Nutzen hat (wer hat wirklich statische Variablen in C verwendet, ohne Fehler zu züchten?)

Bearbeiten :

Baczek machte ein interessantes Beispiel. Zusammen mit den meisten Ihrer Kommentare und insbesondere von Utaal habe ich weiter ausgeführt:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Mir scheint, dass die Entwurfsentscheidung relativ dazu war, wo der Umfang der Parameter platziert werden sollte: innerhalb der Funktion oder "zusammen" damit?

Das Binden innerhalb der Funktion würde bedeuten, dass xes effektiv an den angegebenen Standard gebunden ist, wenn die Funktion aufgerufen und nicht definiert wird, was einen tiefen Fehler darstellen würde: Die defLinie wäre "hybrid" in dem Sinne, dass ein Teil der Bindung (von Das Funktionsobjekt würde bei der Definition und der Teil (Zuweisung von Standardparametern) zum Zeitpunkt des Funktionsaufrufs erfolgen.

Das tatsächliche Verhalten ist konsistenter: Alles in dieser Zeile wird ausgewertet, wenn diese Zeile ausgeführt wird, dh bei der Funktionsdefinition.



4
Ich habe keinen Zweifel daran, dass veränderbare Argumente das Prinzip des geringsten Erstaunens für eine durchschnittliche Person verletzen, und ich habe Anfänger gesehen, die dorthin getreten sind und dann Mailinglisten heldenhaft durch Mailing-Tupel ersetzt haben. Trotzdem stimmen veränderbare Argumente immer noch mit Python Zen (Pep 20) überein und fallen in die Klausel "offensichtlich für Niederländisch" (verstanden / ausgenutzt von Python-Programmierern mit hartem Kern). Die empfohlene Problemumgehung mit Dokumentzeichenfolgen ist die beste, doch der Widerstand gegen Dokumentzeichenfolgen und (geschriebene) Dokumente ist heutzutage nicht so ungewöhnlich. Persönlich würde ich einen Dekorateur bevorzugen (sagen wir @fixed_defaults).
Serge

5
Mein Argument, wenn ich darauf stoße, lautet: "Warum müssen Sie eine Funktion erstellen, die eine veränderbare Variable zurückgibt, die optional eine veränderbare Funktion sein kann, die Sie an die Funktion übergeben würden? Entweder ändert sie eine veränderbare oder erstellt eine neue. Warum benötigen Sie Und warum sollte der Interpreter neu geschrieben werden, damit Sie dies tun können, ohne Ihrem Code drei Zeilen hinzuzufügen? " Weil wir hier über das Umschreiben der Art und Weise sprechen, wie der Interpreter mit Funktionsdefinitionen und Evokationen umgeht. Das ist viel zu tun für einen kaum notwendigen Anwendungsfall.
Alan Leuthard

12
"Python-Neulinge würden erwarten, dass diese Funktion immer eine Liste mit nur einem Element zurückgibt: [5]" Ich bin ein Python - Neuling, und ich würde dies nicht erwarten, denn offensichtlich foo([1])wird zurückkehren [1, 5], nicht [5]. Was Sie damit sagen wollten, ist, dass ein Anfänger erwarten würde, dass die ohne Parameter aufgerufene Funktion immer zurückkehrt [5].
symplektomorph

2
Diese Frage fragt: "Warum wurde dies [auf die falsche Weise] so implementiert?" Es fragt nicht "Was ist der richtige Weg?" , die von [ Warum behebt die Verwendung von arg = None das Problem mit dem veränderlichen Standardargument von Python? ] * ( stackoverflow.com/questions/10676729/… ). Neue Benutzer interessieren sich fast immer weniger für die ersteren und viel mehr für die letzteren, daher ist dies manchmal ein sehr nützlicher Link / Dupe zum Zitieren.
smci

Antworten:


1612

Tatsächlich ist dies kein Konstruktionsfehler, und es liegt nicht an Interna oder Leistung.
Es kommt einfach von der Tatsache, dass Funktionen in Python erstklassige Objekte sind und nicht nur ein Stück Code.

Sobald Sie sich auf diese Weise Gedanken machen, ist dies völlig sinnvoll: Eine Funktion ist ein Objekt, das anhand seiner Definition bewertet wird. Standardparameter sind eine Art "Mitgliedsdaten" und daher kann sich ihr Status von einem Aufruf zum anderen ändern - genau wie bei jedem anderen Objekt.

In jedem Fall hat Effbot eine sehr schöne Erklärung der Gründe für dieses Verhalten in Standardparameterwerte in Python .
Ich fand es sehr klar und empfehle wirklich, es zu lesen, um besser zu wissen, wie Funktionsobjekte funktionieren.


80
Allen, die die obige Antwort lesen, empfehle ich dringend, sich die Zeit zu nehmen, um den verlinkten Effbot-Artikel zu lesen. Neben all den anderen nützlichen Informationen ist der Teil, wie diese Sprachfunktion für das Zwischenspeichern / Speichern von Ergebnissen verwendet werden kann, sehr praktisch zu wissen!
Cam Jackson

85
Selbst wenn es sich um ein erstklassiges Objekt handelt, könnte man sich dennoch ein Design vorstellen, bei dem der Code für jeden Standardwert zusammen mit dem Objekt gespeichert und bei jedem Aufruf der Funktion neu bewertet wird. Ich sage nicht, dass das besser wäre, nur dass Funktionen, die erstklassige Objekte sind, dies nicht vollständig ausschließen.
Gerrit

312
Sorry, aber alles, was als "Die größte WTF in Python" gilt, ist definitiv ein Designfehler . Dies ist irgendwann eine Fehlerquelle für alle , da zunächst niemand dieses Verhalten erwartet - was bedeutet, dass es zunächst nicht so konzipiert sein sollte. Es ist mir egal, durch welche Reifen sie springen mussten, sie sollten Python so entworfen haben, dass Standardargumente nicht statisch sind.
BlueRaja - Danny Pflughoeft

192
Unabhängig davon, ob es sich um einen Konstruktionsfehler handelt oder nicht, scheint Ihre Antwort zu implizieren, dass dieses Verhalten irgendwie notwendig, natürlich und offensichtlich ist, da Funktionen erstklassige Objekte sind und dies einfach nicht der Fall ist. Python hat Verschlüsse. Wenn Sie das Standardargument durch eine Zuweisung in der ersten Zeile der Funktion ersetzen, wird der Ausdruck bei jedem Aufruf ausgewertet (möglicherweise unter Verwendung von Namen, die in einem umschließenden Bereich deklariert sind). Es gibt überhaupt keinen Grund, warum es nicht möglich oder vernünftig wäre, Standardargumente bei jedem Aufruf der Funktion auf genau dieselbe Weise auswerten zu lassen.
Mark Amery

24
Das Design folgt nicht direkt aus functions are objects. In Ihrem Paradigma besteht der Vorschlag darin, die Standardwerte der Funktionen als Eigenschaften und nicht als Attribute zu implementieren.
Bukzor

273

Angenommen, Sie haben den folgenden Code

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Wenn ich die Deklaration von eat sehe, ist es am wenigsten erstaunlich zu denken, dass wenn der erste Parameter nicht angegeben wird, er dem Tupel entspricht ("apples", "bananas", "loganberries")

Allerdings mache ich später im Code so etwas wie

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

Wenn dann Standardparameter bei der Funktionsausführung und nicht bei der Funktionsdeklaration gebunden wären, wäre ich (auf sehr schlechte Weise) erstaunt zu entdecken, dass Früchte geändert wurden. Dies wäre erstaunlicher, als zu entdecken, dass Ihre fooobige Funktion die Liste mutiert hat.

Das eigentliche Problem liegt in veränderlichen Variablen, und alle Sprachen haben dieses Problem bis zu einem gewissen Grad. Hier ist eine Frage: Angenommen, ich habe in Java den folgenden Code:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Verwendet meine Karte nun den Wert des StringBufferSchlüssels, als er in die Karte eingefügt wurde, oder speichert sie den Schlüssel als Referenz? In jedem Fall ist jemand erstaunt; Entweder die Person, die versucht hat, das Objekt aus der MapVerwendung eines Werts herauszuholen, der mit dem Wert identisch ist, mit dem sie es eingegeben hat, oder die Person, die ihr Objekt scheinbar nicht abrufen kann, obwohl der von ihnen verwendete Schlüssel buchstäblich dasselbe Objekt ist Das wurde verwendet, um es in die Map einzufügen (aus diesem Grund erlaubt Python nicht, dass seine veränderlichen integrierten Datentypen als Wörterbuchschlüssel verwendet werden).

Ihr Beispiel ist ein gutes Beispiel für einen Fall, in dem Python-Neulinge überrascht und gebissen werden. Aber ich würde argumentieren, dass wenn wir dies "reparieren" würden, dies nur eine andere Situation schaffen würde, in der sie stattdessen gebissen würden, und dass eine noch weniger intuitiv wäre. Darüber hinaus ist dies beim Umgang mit veränderlichen Variablen immer der Fall; Sie stoßen immer auf Fälle, in denen jemand intuitiv das eine oder andere Verhalten erwarten kann, je nachdem, welchen Code er schreibt.

Ich persönlich mag Pythons aktuellen Ansatz: Standardfunktionsargumente werden ausgewertet, wenn die Funktion definiert ist und dieses Objekt immer der Standard ist. Ich nehme an, sie könnten im Sonderfall eine leere Liste verwenden, aber diese Art von Spezialgehäuse würde noch mehr Erstaunen hervorrufen, ganz zu schweigen davon, dass sie rückwärts inkompatibel sind.


30
Ich denke, es ist eine Frage der Debatte. Sie handeln auf eine globale Variable. Jede Auswertung, die irgendwo in Ihrem Code mit Ihrer globalen Variablen durchgeführt wird, bezieht sich jetzt (korrekt) auf ("Blaubeeren", "Mangos"). Der Standardparameter kann wie in jedem anderen Fall sein.
Stefano Borini

47
Eigentlich glaube ich nicht, dass ich Ihrem ersten Beispiel zustimme. Ich bin mir nicht sicher, ob mir die Idee gefällt, einen solchen Initialisierer überhaupt zu ändern, aber wenn ich das tun würde, würde ich erwarten, dass er sich genau so verhält, wie Sie es beschreiben - indem Sie den Standardwert in ändern ("blueberries", "mangos").
Ben Blank

12
Der Standardparameter ist wie in jedem anderen Fall. Unerwartet ist, dass der Parameter eine globale und keine lokale Variable ist. Dies liegt wiederum daran, dass der Code bei der Funktionsdefinition ausgeführt wird und nicht aufgerufen wird. Sobald Sie das bekommen und dasselbe für den Unterricht gilt, ist es vollkommen klar.
Lennart Regebro

17
Ich finde das Beispiel eher irreführend als brillant. Wenn some_random_function()Appends zu fruitsihm statt der Zuordnung das Verhalten eat() wird ändern. Soviel zum aktuellen wunderbaren Design. Wenn Sie ein Standardargument verwenden, auf das an anderer Stelle verwiesen wird, und dann die Referenz von außerhalb der Funktion ändern, fragen Sie nach Problemen. Die echte WTF ist, wenn Leute ein neues Standardargument definieren (ein Listenliteral oder einen Aufruf eines Konstruktors) und trotzdem Bit bekommen.
Alexis

13
Sie haben globaldas Tupel nur explizit deklariert und neu zugewiesen - es ist absolut nicht überraschend, wenn es danach eatanders funktioniert.
user3467349

241

Der relevante Teil der Dokumentation :

Standardparameterwerte werden bei der Ausführung der Funktionsdefinition von links nach rechts ausgewertet. Dies bedeutet, dass der Ausdruck einmal ausgewertet wird, wenn die Funktion definiert ist, und dass für jeden Aufruf derselbe „vorberechnete“ Wert verwendet wird. Dies ist besonders wichtig, um zu verstehen, wann ein Standardparameter ein veränderbares Objekt ist, z. B. eine Liste oder ein Wörterbuch: Wenn die Funktion das Objekt ändert (z. B. durch Anhängen eines Elements an eine Liste), wird der Standardwert tatsächlich geändert. Dies ist im Allgemeinen nicht beabsichtigt. Eine Möglichkeit, dies zu Noneumgehen, besteht darin, die Standardeinstellung zu verwenden und sie im Hauptteil der Funktion explizit zu testen, z.

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

180
Die Sätze "das ist im Allgemeinen nicht das, was beabsichtigt war" und "ein Weg, dies zu umgehen" riechen so, als würden sie einen Designfehler dokumentieren.
Bukzor

4
@ Matthew: Mir ist klar, aber es ist die Falle nicht wert. Im Allgemeinen werden Styleguides und Linters aus diesem Grund bedingungslose veränderbare Standardwerte als falsch kennzeichnen. Die explizite Möglichkeit, dasselbe zu tun, besteht darin, ein Attribut in die Funktion ( function.data = []) einzufügen oder noch besser ein Objekt zu erstellen.
Bukzor

6
@bukzor: Fallstricke müssen notiert und dokumentiert werden, weshalb diese Frage gut ist und so viele positive Stimmen erhalten hat. Gleichzeitig müssen Fallstricke nicht unbedingt beseitigt werden. Wie viele Python-Anfänger haben eine Liste an eine Funktion übergeben, die sie geändert hat, und waren schockiert, als die Änderungen in der ursprünglichen Variablen angezeigt wurden? Veränderliche Objekttypen sind jedoch wunderbar, wenn Sie verstehen, wie man sie verwendet. Ich denke, es läuft nur auf die Meinung zu dieser besonderen Falle hinaus.
Matthew

33
Der Ausdruck "dies ist im Allgemeinen nicht das, was beabsichtigt war" bedeutet "nicht das, was der Programmierer eigentlich wollte", nicht "nicht das, was Python tun soll".
Holdenweb

4
@holdenweb Wow, ich bin mega spät zur Party. In Anbetracht des Kontexts ist Bukzor völlig richtig: Sie dokumentieren Verhalten / Konsequenzen, die nicht "beabsichtigt" waren, als sie beschlossen, dass die Sprache die Definition der Funktion ausführen soll. Da dies eine unbeabsichtigte Folge ihrer Designentscheidung ist, handelt es sich um einen Designfehler. Wenn es kein Konstruktionsfehler wäre, müsste nicht einmal "ein Weg um dieses Problem" angeboten werden.
code_dredd

118

Ich weiß nichts über das Innenleben des Python-Interpreters (und ich bin auch kein Experte für Compiler und Interpreter), also beschuldigen Sie mich nicht, wenn ich etwas Unempfindliches oder Unmögliches vorschlage.

Vorausgesetzt, dass Python-Objekte veränderbar sind , sollte dies beim Entwerfen der Standardargumente berücksichtigt werden. Wenn Sie eine Liste instanziieren:

a = []

Sie erwarten eine neue Liste, auf die verwiesen wird a.

Warum sollte das a=[]in

def x(a=[]):

eine neue Liste zur Funktionsdefinition und nicht zum Aufruf instanziieren? Es ist so, als würden Sie fragen: "Wenn der Benutzer das Argument nicht bereitstellt, instanziieren Sie eine neue Liste und verwenden Sie sie so, als ob sie vom Anrufer erstellt wurde." Ich denke, das ist stattdessen mehrdeutig:

def x(a=datetime.datetime.now()):

Benutzer, möchten Sie astandardmäßig die Datums- und Uhrzeitangabe festlegen, die der Definition oder Ausführung entspricht x? In diesem Fall werde ich wie im vorherigen das gleiche Verhalten beibehalten, als ob das Standardargument "Zuweisung" die erste Anweisung der Funktion wäre (beim Funktionsaufruf datetime.now()aufgerufen). Wenn der Benutzer andererseits die Definition-Zeit-Zuordnung wünschte, konnte er schreiben:

b = datetime.datetime.now()
def x(a=b):

Ich weiß, ich weiß: das ist eine Schließung. Alternativ kann Python ein Schlüsselwort bereitstellen, um die Bindung der Definitionszeit zu erzwingen:

def x(static a=b):

11
Sie könnten tun: def x (a = None): Und wenn a None ist, setzen Sie a = datetime.datetime.now ()
Anon

20
Danke dafür. Ich konnte nicht wirklich sagen, warum mich das bis zum Äußersten ärgert. Sie haben es wunderbar mit einem Minimum an Flaum und Verwirrung gemacht. Als jemand, der aus der Systemprogrammierung in C ++ stammt und manchmal naiv Sprachfunktionen "übersetzt", hat mich dieser falsche Freund genau wie Klassenattribute in den Kopf getreten. Ich verstehe, warum die Dinge so sind, aber ich kann nicht anders, als es nicht zu mögen, egal was positiv daraus werden mag. Zumindest widerspricht es meiner Erfahrung, dass ich es wahrscheinlich (hoffentlich) nie vergessen werde ...
AndreasT

5
@Andreas Sobald Sie Python lange genug verwenden, werden Sie feststellen, wie logisch es für Python ist, Dinge so wie sie als Klassenattribute zu interpretieren - dies liegt nur an den besonderen Macken und Einschränkungen von Sprachen wie C ++ (und Java, und C # ...) dass es keinen Sinn macht, den Inhalt des class {}Blocks als zu den Instanzen gehörend zu interpretieren :) Aber wenn Klassen erstklassige Objekte sind, ist es natürlich selbstverständlich, dass ihr Inhalt (im Speicher) ihren Inhalt widerspiegelt (in Code).
Karl Knechtel

6
Normative Struktur ist in meinem Buch keine Eigenart oder Einschränkung. Ich weiß, dass es ungeschickt und hässlich sein kann, aber man kann es eine "Definition" von etwas nennen. Die dynamischen Sprachen scheinen mir ein bisschen wie Anarchisten zu sein: Sicher, jeder ist frei, aber man braucht Struktur, um jemanden dazu zu bringen, den Müll zu leeren und die Straße zu ebnen. Ich denke ich bin alt ... :)
AndreasT

4
Die Funktion Definition wird auf Modulladezeit ausgeführt. Die Funktion Körper wird bei Funktionsaufruf Zeit ausgeführt. Das Standardargument ist Teil der Funktionsdefinition und nicht des Funktionskörpers. (Für verschachtelte Funktionen wird es komplizierter.)
Lutz Prechelt

84

Der Grund ist ganz einfach, dass Bindungen ausgeführt werden, wenn Code ausgeführt wird, und die Funktionsdefinition ausgeführt wird, na ja ... wenn die Funktionen definiert sind.

Vergleichen Sie dies:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Dieser Code leidet unter genau demselben unerwarteten Zufall. Bananen sind ein Klassenattribut. Wenn Sie also Dinge hinzufügen, werden sie allen Instanzen dieser Klasse hinzugefügt. Der Grund ist genau der gleiche.

Es ist nur "Wie es funktioniert", und es im Funktionsfall anders zu machen, wäre wahrscheinlich kompliziert und im Klassenfall wahrscheinlich unmöglich, oder zumindest die Objektinstanziierung stark zu verlangsamen, da Sie den Klassencode beibehalten müssten und führen Sie es aus, wenn Objekte erstellt werden.

Ja, das ist unerwartet. Aber sobald der Penny fällt, passt er perfekt zu Python im Allgemeinen. Tatsächlich ist es eine gute Lehrhilfe, und wenn Sie erst einmal verstanden haben, warum dies passiert, werden Sie Python viel besser verstehen.

Das heißt, es sollte in jedem guten Python-Tutorial eine herausragende Rolle spielen. Denn wie Sie bereits erwähnt haben, stößt jeder früher oder später auf dieses Problem.


Wie definieren Sie ein Klassenattribut, das für jede Instanz einer Klasse unterschiedlich ist?
Kieveli

19
Wenn es für jede Instanz unterschiedlich ist, ist es kein Klassenattribut. Klassenattribute sind Attribute in der KLASSE. Daher der Name. Daher sind sie für alle Fälle gleich.
Lennart Regebro

1
Wie definieren Sie ein Attribut in einer Klasse, das für jede Instanz einer Klasse unterschiedlich ist? (Neu definiert für diejenigen, die nicht feststellen konnten, dass eine Person, die mit Pythons Namenskonventionen nicht vertraut ist, möglicherweise nach normalen Mitgliedsvariablen einer Klasse fragt.)
Kieveli

@ Kievieli: Sie sprechen über normale Mitgliedsvariablen einer Klasse. :-) Sie definieren Instanzattribute, indem Sie in jeder Methode self.attribute = value sagen. Zum Beispiel __init __ ().
Lennart Regebro

@Kieveli: Zwei Antworten: Sie können nicht, da alles, was Sie auf Klassenebene definieren, ein Klassenattribut ist und jede Instanz, die auf dieses Attribut zugreift, auf dasselbe Klassenattribut zugreift. Sie können / sort of / verwenden, indem Sie propertys verwenden - das sind Funktionen auf Klassenebene, die sich wie normale Attribute verhalten, aber das Attribut in der Instanz anstelle der Klasse speichern (indem Sie, self.attribute = valuewie Lennart sagte, verwenden).
Ethan Furman

66

Warum siehst du nicht nach innen?

Ich bin wirklich überrascht, dass niemand die aufschlussreiche Selbstbeobachtung durchgeführt hat, die Python ( 2und die 3Anwendung) für Callables bietet .

Gegeben eine einfache kleine Funktion funcdefiniert als:

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

Wenn Python darauf stößt, wird es zunächst kompiliert, um ein codeObjekt für diese Funktion zu erstellen . Während dieser Kompilierungsschritt wertet Python * aus und speichert die Standardargumente (hier eine leere Liste ) im Funktionsobjekt selbst[] . Wie in der Top-Antwort erwähnt: Die Liste akann nun als Mitglied der Funktion betrachtet werden func.

Lassen Sie uns also vorher und nachher eine Selbstbeobachtung durchführen, um zu untersuchen, wie die Liste innerhalb des Funktionsobjekts erweitert wird. Ich benutze Python 3.xdafür, für Python 2 gilt das Gleiche (benutze __defaults__oder func_defaultsin Python 2; ja, zwei Namen für das gleiche Ding).

Funktion vor der Ausführung:

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

Nachdem Python diese Definition ausgeführt hat, werden alle a = []hier angegebenen Standardparameter verwendet und im __defaults__Attribut für das Funktionsobjekt (relevanter Abschnitt: Callables) gespeichert:

>>> func.__defaults__
([],)

Ok, also eine leere Liste als einzelner Eintrag in __defaults__, genau wie erwartet.

Funktion nach Ausführung:

Lassen Sie uns nun diese Funktion ausführen:

>>> func()

Nun wollen wir diese noch __defaults__einmal sehen:

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

Erstaunt? Der Wert im Objekt ändert sich! Aufeinanderfolgende Aufrufe der Funktion werden jetzt einfach an das eingebettete listObjekt angehängt :

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

Der Grund, warum dieser 'Fehler' auftritt, liegt darin, dass Standardargumente Teil des Funktionsobjekts sind. Hier ist nichts Seltsames los, es ist alles nur ein bisschen überraschend.

Die übliche Lösung, um dem entgegenzuwirken, besteht darin, Noneals Standard zu verwenden und dann im Funktionskörper zu 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 übergeben wurde a.


Um weiter zu überprüfen, ob die Liste in __defaults__mit der in der Funktion verwendeten funcübereinstimmt, können Sie einfach Ihre Funktion ändern, um iddie Liste zurückzugeben, adie im Funktionskörper verwendet wird. Vergleichen Sie es dann mit der Liste in __defaults__(Position [0]in __defaults__) und Sie werden sehen, wie sich diese tatsächlich auf dieselbe Listeninstanz beziehen:

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

Alles mit der Kraft der Selbstbeobachtung!


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

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 es aufgerufen, bevor die Funktion erstellt und an den Namen gebunden barwird.


1
Wird id(...)für diese letzte Überprüfung benötigt oder würde der isBediener dieselbe Frage beantworten?
das-g

1
@ das-g iswürde gut funktionieren, ich habe es nur verwendet, id(val)weil ich denke, dass es intuitiver sein könnte.
Dimitris Fasarakis Hilliard

Die Verwendung Noneals Standard schränkt die Nützlichkeit der __defaults__Selbstbeobachtung stark ein, daher denke ich nicht, dass dies gut funktioniert, __defaults__um die Arbeit so zu verteidigen, wie sie funktioniert. Eine verzögerte Auswertung würde mehr dazu beitragen, die Funktionsstandards von beiden Seiten nützlich zu halten.
Brilliand

58

Früher dachte ich, dass das Erstellen der Objekte zur Laufzeit der bessere Ansatz wäre. Ich bin mir jetzt weniger sicher, da Sie einige nützliche Funktionen verlieren, obwohl es sich lohnen kann, nur um Verwirrung bei Neulingen zu vermeiden. Die Nachteile dabei sind:

1. Leistung

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

Wenn die Auswertung der Anrufzeit verwendet wird, wird die teure Funktion jedes Mal aufgerufen, wenn Ihre Funktion ohne Argument verwendet wird. Sie zahlen entweder einen teuren Preis für jeden Anruf oder müssen den Wert manuell extern zwischenspeichern, Ihren Namespace verschmutzen und Ausführlichkeit hinzufügen.

2. Erzwingen gebundener Parameter

Ein nützlicher Trick besteht darin, Parameter eines Lambdas an die aktuelle Bindung einer Variablen zu binden , wenn das Lambda erstellt wird. 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, werden sie stattdessen ian den Aufrufzeitwert von i gebunden, sodass Sie eine Liste der Funktionen erhalten, die alle zurückgegeben wurden 9.

Die einzige Möglichkeit, dies anderweitig zu implementieren, besteht darin, einen weiteren Abschluss mit der i-Grenze zu erstellen, dh:

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

3. Selbstbeobachtung

Betrachten Sie den Code:

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

Informationen über die Argumente und Standardeinstellungen können wir mit dem inspectModul abrufen

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

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

Angenommen, das Verhalten der Standardeinstellungen könnte so geändert werden, dass dies dem Äquivalent von:

_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=[]

Allerdings haben wir die Möglichkeit , selbst zu überwachen, verloren und sehen , was die Standardargumente sind . Da die Objekte nicht erstellt wurden, können wir sie niemals erreichen, ohne die Funktion tatsächlich aufzurufen. Das Beste, was wir tun können, ist, den Quellcode zu speichern und als Zeichenfolge zurückzugeben.


1
Sie könnten auch eine Selbstbeobachtung erreichen, wenn für jede Funktion das Standardargument anstelle eines Werts erstellt würde. Das Inspect-Modul ruft nur diese Funktion auf.
Yairchu

@SilentGhost: Ich spreche darüber, ob das Verhalten geändert wurde, um es neu zu erstellen. Das einmalige Erstellen ist das aktuelle Verhalten und warum das veränderbare Standardproblem besteht.
Brian

1
@yairchu: Das setzt voraus, dass die Konstruktion dazu sicher ist (dh keine Nebenwirkungen hat). Introspecting die args nicht sollten tun nichts, aber beliebigen Code Auswertung eine Wirkung könnte auch am Ende.
Brian

1
Ein anderes Sprachdesign bedeutet oft nur, Dinge anders zu schreiben. Ihr erstes Beispiel könnte leicht geschrieben werden als: _expensive = teuer (); def foo (arg = _expensive), wenn Sie nicht möchten, dass es neu bewertet wird.
Glenn Maynard

@Glenn - darauf habe ich mich bei "Externe Cache der Variablen" bezogen - es ist etwas ausführlicher, und Sie haben am Ende zusätzliche Variablen in Ihrem Namespace.
Brian

55

5 Punkte zur Verteidigung von Python

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

  2. Konsistenz : Python übergibt immer Objekte, keine Namen. Der Standardparameter ist offensichtlich Teil der Funktionsüberschrift (nicht des Funktionskörpers). Es sollte daher zur Ladezeit des Moduls (und nur zur Ladezeit des Moduls, sofern nicht verschachtelt) und nicht zur Funktionsaufrufzeit ausgewertet werden.

  3. Nützlichkeit : Wie Frederik Lundh in seiner Erklärung zu "Standardparameterwerte in Python" ausführt , kann das aktuelle Verhalten für die erweiterte Programmierung sehr nützlich sein. (Sparsam verwenden.)

  4. Ausreichende Dokumentation : In der grundlegendsten Python-Dokumentation, dem Lernprogramm, wird das Problem im ersten Unterabschnitt des Abschnitts "Weitere Informationen zum Definieren von Funktionen" lautstark als "Wichtige Warnung" angekündigt . Die Warnung verwendet sogar Fettdruck, der nur selten außerhalb von Überschriften angewendet wird. RTFM: Lesen Sie das feine Handbuch.

  5. Meta-Lernen : In die Falle zu tappen ist tatsächlich ein sehr hilfreicher Moment (zumindest wenn Sie ein reflektierender Lernender sind), da Sie anschließend den obigen Punkt "Konsistenz" besser verstehen und dadurch viel über Python lernen.


18
Ich habe ein Jahr gebraucht, um festzustellen, dass dieses Verhalten meinen Code in der Produktion durcheinander bringt. Am Ende habe ich eine komplette Funktion entfernt, bis ich zufällig auf diesen Konstruktionsfehler gestoßen bin. Ich benutze Django. Da die Staging-Umgebung nicht viele Anforderungen hatte, hatte dieser Fehler keine Auswirkungen auf die Qualitätssicherung. Als wir live gingen und viele gleichzeitige Anfragen erhielten, begannen einige Dienstprogrammfunktionen, die Parameter des anderen zu überschreiben! Sicherheitslücken, Fehler und was nicht.
Oriadam

7
@oriadam, nichts für ungut, aber ich frage mich, wie Sie Python gelernt haben, ohne zuvor darauf gestoßen zu sein. Ich lerne gerade Python und diese mögliche Gefahr wird im offiziellen Python-Tutorial direkt neben der ersten Erwähnung von Standardargumenten erwähnt. (Wie in Punkt 4 dieser Antwort erwähnt.) Ich nehme an, die Moral ist - ziemlich unsympathisch -, die offiziellen Dokumente der Sprache zu lesen, die Sie zum Erstellen von Produktionssoftware verwenden.
Wildcard

Es wäre auch (für mich) überraschend, wenn zusätzlich zu dem Funktionsaufruf, den ich mache, eine Funktion unbekannter Komplexität aufgerufen würde.
Vatine

52

Dieses Verhalten lässt sich leicht erklären durch:

  1. Die Funktionsdeklaration (Klasse usw.) wird nur einmal ausgeführt, wodurch alle Standardwertobjekte erstellt werden
  2. Alles wird als Referenz übergeben

Damit:

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 erstellt ein neues int-Objekt - ein neues Objekt wird gedruckt
  2. b ändert sich nicht - neues Array wird aus dem Standardwert erstellt und gedruckt
  3. c Änderungen - Operation wird an demselben Objekt ausgeführt - und es wird gedruckt

(Eigentlich ist add ein schlechtes Beispiel, aber Ganzzahlen, die unveränderlich sind, sind immer noch mein Hauptpunkt.)
Anon

Ich habe es zu meinem Leidwesen erkannt, nachdem ich überprüft habe, dass mit b auf [] gesetzt, b .__ add __ ([1]) [1] zurückgibt, aber auch b immer noch [] lässt, obwohl Listen veränderlich sind. Mein Fehler.
Anon

@ANon: gibt es __iadd__, aber es funktioniert nicht mit int. Na sicher. :-)
Veky

35

Was Sie fragen, ist, warum dies:

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

ist intern nicht gleichbedeutend damit:

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()

mit Ausnahme des expliziten Aufrufs von func (None, None), den wir ignorieren werden.

Mit anderen Worten, anstatt Standardparameter auszuwerten, warum nicht jeden von ihnen speichern und sie auswerten, wenn die Funktion aufgerufen wird?

Eine Antwort ist wahrscheinlich genau dort - sie würde effektiv jede Funktion mit Standardparametern in einen Abschluss verwandeln. Selbst wenn alles im Interpreter versteckt ist und kein vollständiger Abschluss vorliegt, müssen die Daten irgendwo gespeichert werden. Es wäre langsamer und würde mehr Speicher verbrauchen.


6
Es müsste kein Abschluss sein - eine bessere Möglichkeit, es sich vorzustellen, wäre einfach, den Bytecode, der die Standardeinstellungen erstellt, zur ersten Codezeile zu machen - schließlich kompilieren Sie den Body ohnehin zu diesem Zeitpunkt - es gibt keinen wirklichen Unterschied zwischen Code in den Argumenten und Code im Körper.
Brian

10
Stimmt, aber es würde Python immer noch verlangsamen, und es wäre tatsächlich ziemlich überraschend, wenn Sie nicht dasselbe für Klassendefinitionen tun, was es dumm langsam machen würde, da Sie die gesamte Klassendefinition jedes Mal neu ausführen müssten, wenn Sie a instanziieren Klasse. Wie bereits erwähnt, wäre die Lösung überraschender als das Problem.
Lennart Regebro

Einverstanden mit Lennart. Wie Guido gern sagt, gibt es für jede Sprachfunktion oder Standardbibliothek jemanden , der sie verwendet.
Jason Baker

6
Es jetzt zu ändern wäre Wahnsinn - wir untersuchen nur, warum es so ist, wie es ist. Wenn zunächst eine verspätete Standardbewertung durchgeführt würde, wäre dies nicht unbedingt überraschend. Es ist definitiv wahr, dass ein solcher Kern, ein Unterschied beim Parsen, weitreichende und wahrscheinlich viele obskure Auswirkungen auf die Sprache als Ganzes haben würde.
Glenn Maynard

35

1) Das sogenannte Problem des "veränderlichen Standardarguments" ist im Allgemeinen ein spezielles Beispiel, das zeigt, dass:
"Alle Funktionen mit diesem Problem leiden auch unter einem ähnlichen Nebenwirkungsproblem des tatsächlichen Parameters ."
das gegen die Regeln der funktionalen Programmierung verstößt. in der Regel nicht zu beachten und sollte beide zusammen befestigt 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 : eine Kopie
Eine absolut sichere Lösung besteht darin, zuerst das Eingabeobjekt copyoder deepcopydas Eingabeobjekt zu erstellen und dann alles mit der Kopie zu tun.

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

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

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 weder in einem öffentlichen Attribut einer von dieser Funktion zurückgegebenen Instanz gespeichert werden. (Angenommen, private Attribute der Instanz sollten nicht von außerhalb dieser Klasse oder Unterklassen gemäß Konvention geändert werden. Dies _var1ist ein privates Attribut.)

Schlussfolgerung:
Eingabeparameterobjekte sollten nicht an Ort und Stelle geändert (mutiert) oder an ein von der Funktion zurückgegebenes Objekt gebunden werden. (Wenn wir das Programmieren ohne Nebenwirkungen bevorzugen, was dringend empfohlen wird. Siehe Wiki über "Nebenwirkungen" (Die ersten beiden Absätze sind in diesem Zusammenhang relevant.)

2)
Nur wenn die Nebenwirkung auf den tatsächlichen Parameter erforderlich, aber auf den Standardparameter unerwünscht ist, ist die nützliche Lösung def ...(var1=None): if var1 is None: var1 = [] Mehr.

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


5
Ich hoffe, Sie wissen, dass Python keine funktionierende Programmiersprache ist.
Veky

6
Ja, Python ist eine Multi-Paragigm-Sprache mit einigen Funktionsmerkmalen. ("Lassen Sie nicht jedes Problem wie einen Nagel aussehen, nur weil Sie einen Hammer haben.") Viele von ihnen sind in Python Best Practices enthalten. Python hat eine interessante HOWTO-Funktionsprogrammierung. Weitere Funktionen sind Verschlüsse und Currying, die hier nicht erwähnt werden.
Hynekcer

1
Zu diesem späten Zeitpunkt möchte ich auch hinzufügen, dass die Zuweisungssemantik von Python explizit so konzipiert wurde, dass das Kopieren von Daten bei Bedarf vermieden wird, sodass die Erstellung von Kopien (und insbesondere von tiefen Kopien) sowohl die Laufzeit als auch die Speichernutzung nachteilig beeinflusst. Sie sollten daher nur bei Bedarf verwendet werden, aber Neuankömmlinge haben oft Schwierigkeiten zu verstehen, wann dies der Fall ist.
Holdenweb

1
@holdenweb Ich stimme zu. Eine temporäre Kopie ist der üblichste und manchmal der einzig mögliche Weg, um die ursprünglichen veränderlichen Daten vor einer fremden Funktion zu schützen, die sie möglicherweise ändert. Glücklicherweise wird eine Funktion, die Daten unangemessen ändert, als Fehler angesehen und ist daher ungewöhnlich.
Hynekcer

Ich stimme dieser Antwort zu. Und ich verstehe nicht, warum das def f( a = None )Konstrukt empfohlen wird, wenn Sie wirklich etwas anderes meinen. Das Kopieren ist in Ordnung, da Sie Argumente nicht mutieren sollten. Und wenn Sie dies if a is None: a = [1, 2, 3]tun, kopieren Sie die Liste trotzdem.
Koddo

30

Dies hat eigentlich nichts mit Standardwerten zu tun, außer dass es häufig als unerwartetes Verhalten auftritt, wenn Sie Funktionen mit veränderlichen 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 erhalten genau das gleiche Problem.

Das Problem ist , dass foosich Modifizierung eine veränderbare Variable in dem Aufrufer übergeben, wenn der Anrufer dies nicht erwarten. Code wie dieser wäre in Ordnung, wenn die Funktion so etwas wie aufgerufen würdeappend_5 ; dann würde der Aufrufer die Funktion aufrufen, um den übergebenen Wert zu ändern, und das Verhalten würde erwartet. Es ist jedoch sehr unwahrscheinlich, dass eine solche Funktion ein Standardargument annimmt und die Liste wahrscheinlich nicht zurückgibt (da der Aufrufer bereits einen Verweis auf diese Liste hat; die, die er gerade übergeben hat).

Ihr Original foomit einem Standardargument sollte nicht ändern, aob es explizit übergeben wurde oder den Standardwert erhalten hat. Ihr Code sollte veränderbare Argumente in Ruhe lassen, es sei denn, aus dem Kontext / Namen / der Dokumentation geht hervor, dass die Argumente geändert werden sollen. Die Verwendung von veränderlichen Werten, die als Argumente als lokale temporäre Werte übergeben werden, ist eine äußerst schlechte Idee, unabhängig davon, ob wir uns in Python befinden oder nicht und ob es sich um Standardargumente handelt oder nicht.

Wenn Sie ein lokales temporäres Element im Verlauf der Berechnung destruktiv manipulieren müssen und Ihre Manipulation von einem Argumentwert aus starten müssen, müssen Sie eine Kopie erstellen.


7
Obwohl verwandt, denke ich, dass dies ein unterschiedliches Verhalten ist (da wir erwarten append, dass es sich a"an Ort und Stelle" ändert ). Dass ein Standard-Mutable nicht bei jedem Aufruf erneut instanziiert wird, ist das "unerwartete" Bit ... zumindest für mich. :)
Andy Hayden

2
@AndyHayden Wenn von der Funktion erwartet wird, dass sie das Argument ändert, warum ist es dann sinnvoll, einen Standardwert zu haben?
Mark Ransom

@ MarkRansom das einzige Beispiel, an das ich denken kann, ist cache={}. Ich vermute jedoch, dass dieses "geringste Erstaunen" auftritt, wenn Sie die Funktion, die Sie aufrufen, um das Argument zu mutieren, nicht erwarten (oder wollen).
Andy Hayden

1
@AndyHayden Ich habe hier meine eigene Antwort mit einer Erweiterung dieses Gefühls hinterlassen. Lass mich wissen was du denkst. Der cache={}Vollständigkeit halber möchte ich Ihr Beispiel hinzufügen .
Mark Ransom

1
@AndyHayden Der Punkt meiner Antwort ist, dass, wenn Sie jemals erstaunt sind, den Standardwert eines Arguments versehentlich zu mutieren, Sie einen weiteren Fehler haben, nämlich dass Ihr Code den Wert eines Anrufers versehentlich mutieren kann, wenn der Standardwert nicht verwendet wurde. Und beachten Sie, dass die Verwendung Noneund Zuweisung des tatsächlichen Standards, wenn das Argument aktiviert ist None , dieses Problem nicht löst (ich halte es aus diesem Grund für ein Anti-Muster). Wenn Sie den anderen Fehler beheben, indem Sie mutierende Argumentwerte vermeiden, unabhängig davon, ob sie Standardeinstellungen haben oder nicht, werden Sie dieses "erstaunliche" Verhalten nie bemerken oder sich darum kümmern.
Ben

27

Bereits beschäftigtes Thema, aber nach dem, was ich hier gelesen habe, hat mir Folgendes 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

2
Tatsächlich könnte dies für Neulinge etwas verwirrend sein, da sie a = a + [1]überlastet sind. aErwägen Sie, es zu ändern b = a + [1] ; print id(b)und eine Zeile hinzuzufügen a.append(2). Dadurch wird deutlicher, dass +auf zwei Listen immer eine neue Liste (zugewiesen b) erstellt wird, während eine geänderte Liste aimmer noch dieselbe haben kann id(a).
Jörn Hees

25

Es ist eine Leistungsoptimierung. Welcher dieser 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 dir einen Hinweis. 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 erlebte Verhalten einen praktischen Nutzen hat (wer hat wirklich statische Variablen in C verwendet, ohne Fehler zu züchten?)

Wie Sie sehen, bietet die Verwendung unveränderlicher Standardargumente einen Leistungsvorteil. Dies kann einen Unterschied machen, wenn es sich um eine häufig aufgerufene Funktion handelt oder die Erstellung des Standardarguments lange dauert. Denken Sie auch daran, dass Python nicht C ist. In C haben Sie Konstanten, die ziemlich frei sind. In Python haben Sie diesen Vorteil nicht.


24

Python: Das veränderbare Standardargument

Standardargumente werden zum Zeitpunkt der Kompilierung der Funktion zu einem Funktionsobjekt ausgewertet. Wenn sie von der Funktion mehrfach verwendet werden, sind und bleiben sie dasselbe Objekt.

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

Sie bleiben mutiert, weil sie jedes Mal dasselbe Objekt sind.

Äquivalenter Code:

Da die Liste beim Kompilieren und Instanziieren des Funktionsobjekts an die Funktion gebunden ist, gilt Folgendes:

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

ist fast genau gleichbedeutend damit:

_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 ist eine Demonstration: Sie können jedes Mal, wenn auf sie verwiesen wird, überprüfen, ob es sich um dasselbe Objekt handelt

  • zu sehen, dass die Liste erstellt wird, bevor die Funktion zu einem Funktionsobjekt kompiliert wurde,
  • Beachten Sie, dass die ID jedes Mal, wenn auf die Liste verwiesen wird, dieselbe ist.
  • Beachten Sie, dass die Liste geändert bleibt, wenn die Funktion, die sie verwendet, ein zweites Mal aufgerufen wird.
  • Beobachten Sie die Reihenfolge, in der die Ausgabe von der Quelle gedruckt wird (die ich bequem 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 laufen lassen 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 das Prinzip des "geringsten Erstaunens"?

Diese Ausführungsreihenfolge ist für neue Benutzer von Python häufig verwirrend. Wenn Sie das Python-Ausführungsmodell verstehen, wird es durchaus erwartet.

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

Aus diesem Grund besteht die übliche Anweisung für neue Benutzer darin, stattdessen ihre Standardargumente wie folgt zu erstellen:

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

Dies verwendet den None-Singleton als Sentinel-Objekt, um der Funktion mitzuteilen, ob wir ein anderes Argument als das Standardargument erhalten haben oder nicht. Wenn wir kein Argument erhalten, möchten wir []standardmäßig eine neue leere Liste verwenden .

Im Tutorial-Abschnitt zum Kontrollfluss heißt es:

Wenn Sie nicht möchten, dass die Standardeinstellung für nachfolgende Aufrufe freigegeben 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

24

Die kürzeste Antwort wäre wahrscheinlich "Definition ist Ausführung", daher macht das ganze Argument keinen strengen Sinn. Als ein besseres Beispiel können Sie Folgendes anführen:

def a(): return []

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

Hoffentlich reicht es zu zeigen, dass defes nicht einfach oder nicht sinnvoll ist, die Standardargumentausdrücke zum Ausführungszeitpunkt der Anweisung nicht auszuführen , oder beides.

Ich bin damit einverstanden, dass es ein Problem ist, wenn Sie versuchen, Standardkonstruktoren zu verwenden.


20

Eine einfache Problemumgehung 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]

19

Dieses Verhalten ist nicht überraschend, wenn Sie Folgendes berücksichtigen:

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

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

(1) wird im Python- Tutorial zu Klassen beschrieben . Bei dem Versuch, einem schreibgeschützten Klassenattribut einen Wert zuzuweisen:

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

Schauen Sie zurück zum ursprünglichen Beispiel und beachten Sie die obigen Punkte:

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

Hier fooist ein Objekt und aist ein Attribut von foo(verfügbar unter foo.func_defs[0]). Da aist eine Liste, aist veränderlich und ist somit ein Lese- / Schreibattribut von foo. Es wird in der leeren Liste initialisiert, wie in der Signatur angegeben, wenn die Funktion instanziiert wird, und steht zum Lesen und Schreiben zur Verfügung, solange das Funktionsobjekt vorhanden ist.

Beim Aufrufen fooohne Überschreiben eines Standards wird der Wert dieses Standards von verwendet foo.func_defs. In diesem Fall foo.func_defs[0]wird ainnerhalb des Codebereichs des Funktionsobjekts verwendet. Änderungen an aÄnderungen foo.func_defs[0], die Teil des fooObjekts sind und zwischen der Ausführung des Codes in bestehen bleiben foo.

Vergleichen Sie dies nun mit dem Beispiel aus der Dokumentation zum Emulieren des Standardargumentverhaltens anderer Sprachen , sodass die Standardeinstellungen für die Funktionssignatur bei jeder Ausführung der Funktion verwendet werden:

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

Unter Berücksichtigung von (1) und (2) kann man sehen, warum dies das gewünschte Verhalten bewirkt:

  • Wenn das fooFunktionsobjekt instanziiert wird, foo.func_defs[0]wird es auf Noneein unveränderliches Objekt gesetzt.
  • Wenn die Funktion mit Standardwerten ausgeführt wird (ohne dass Lim Funktionsaufruf ein Parameter angegeben wurde ), ist foo.func_defs[0]( None) im lokalen Bereich als verfügbar L.
  • Nach L = []kann die Zuweisung nicht erfolgreich sein foo.func_defs[0], da dieses Attribut schreibgeschützt ist.
  • Per (1) , eine neue lokale Variable auch genannt Lim lokalen Bereich erstellt wird , und für den Rest des Funktionsaufrufes. foo.func_defs[0]bleibt somit für zukünftige Aufrufe von unverändert foo.

19

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

Wie andere ausführlich kommentiert haben, ist der Listenparameter an die Funktion gebunden, wenn er definiert ist, und nicht, wenn er ausgeführt wird. Da Listen und Wörterbücher veränderbar sind, wirkt sich jede Änderung dieses Parameters auf andere Aufrufe dieser Funktion aus. Infolgedessen erhalten nachfolgende Aufrufe der Funktion diese gemeinsam genutzte Liste, die möglicherweise durch andere Aufrufe der Funktion geändert wurde. Schlimmer noch, zwei Parameter verwenden gleichzeitig den gemeinsamen Parameter dieser Funktion, ohne die Änderungen zu berücksichtigen, die 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 Folgendes verwenden id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Per Brett Slatkins "Effektives Python: 59 spezifische Möglichkeiten, besseres Python zu schreiben", Punkt 20: Verwenden Sie Noneund Docstrings, um dynamische Standardargumente anzugeben (S. 48).

Die Konvention zum Erreichen des gewünschten Ergebnisses in Python besteht darin, einen Standardwert von bereitzustellen Noneund das tatsächliche Verhalten in der Dokumentzeichenfolge zu dokumentieren.

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

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, bei denen der Programmierer beabsichtigte, den Standardlistenparameter gemeinsam zu nutzen. Dies ist jedoch eher die Ausnahme als die Regel.


17

Die Lösungen hier sind:

  1. Verwenden Sie Noneals Standardwert (oder als Nonce object) und aktivieren Sie diesen Wert , um Ihre Werte zur Laufzeit zu erstellen. oder
  2. Verwenden Sie a lambdaals Standardparameter und rufen Sie es in einem try-Block auf, um den Standardwert abzurufen (für diese Art der Lambda-Abstraktion ist dies vorgesehen).

Die zweite Option ist hilfreich, da Benutzer der Funktion eine aufrufbare Datei übergeben können, die möglicherweise bereits vorhanden ist (z. B. a type).


16

Wenn wir das tun:

def foo(a=[]):
    ...

... ordnen wir das Argument aeiner unbenannten Liste zu, wenn der Aufrufer den Wert von a nicht übergibt.

Um die Diskussion zu vereinfachen, geben wir der unbenannten Liste vorübergehend einen Namen. Wie wäre es pavlo?

def foo(a=pavlo):
   ...

Wenn der Anrufer uns nicht sagt, was aist, können wir jederzeit wiederverwenden pavlo.

Wenn pavloes veränderbar (modifizierbar) ist und es fooletztendlich modifiziert, wird ein Effekt, den wir beim nächsten Mal bemerken foo, ohne Angabe aufgerufen a.

Das sehen Sie also (Denken Sie daran, pavlowird mit [] initialisiert):

 >>> foo()
 [5]

Nun pavloist [5].

Erneutes Aufrufen foo()ändert sich pavloerneut:

>>> foo()
[5, 5]

Die Angabe abeim Anrufen foo()stellt sicher, dass pavlonicht berührt wird.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Also pavloist es immer noch [5, 5].

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

16

Ich nutze dieses Verhalten manchmal als Alternative zum folgenden Muster:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Wenn singletonnur von verwendet wird use_singleton, mag ich das folgende Muster als Ersatz:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

Ich habe dies verwendet, um Clientklassen zu instanziieren, die auf externe Ressourcen zugreifen, und um Diktate oder Listen zum Auswendiglernen zu erstellen.

Da ich dieses Muster nicht für bekannt halte, füge ich einen kurzen Kommentar hinzu, um zukünftige Missverständnisse zu vermeiden.


2
Ich ziehe es vor, einen Dekorator für die Memoisierung hinzuzufügen und den Memoisierungscache auf das Funktionsobjekt selbst zu setzen.
Stefano Borini

Dieses Beispiel ersetzt nicht das komplexere Muster, das Sie anzeigen, da Sie _make_singletonim Standardargumentbeispiel zur def-Zeit aufrufen, im globalen Beispiel jedoch zur Aufrufzeit. Eine echte Substitution würde eine Art veränderbares Feld für den Standardargumentwert verwenden, aber das Hinzufügen des Arguments bietet die Möglichkeit, alternative Werte zu übergeben.
Yann Vernier

15

Sie können dies umgehen, indem Sie das Objekt (und damit die Verbindung zum Zielfernrohr) ersetzen:

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

Hässlich, aber es funktioniert.


3
Dies ist eine gute Lösung in Fällen, in denen Sie eine automatische Dokumentationserstellungssoftware verwenden, um die von der Funktion erwarteten Argumenttypen zu dokumentieren. Wenn Sie a = None setzen und dann a auf [] setzen, wenn a None ist, kann der Leser auf einen Blick nicht verstehen, was erwartet wird.
Michael Scott Cuthbert

Coole Idee: Das erneute Binden dieses Namens garantiert, dass er niemals geändert werden kann. Das gefällt mir sehr.
Holdenweb

Dies ist genau der richtige Weg. Python erstellt keine Kopie des Parameters, daher liegt es an Ihnen, die Kopie explizit zu erstellen. Sobald Sie eine Kopie haben, können Sie diese nach Belieben ohne unerwartete Nebenwirkungen ändern.
Mark Ransom

13

Es kann wahr sein, dass:

  1. Jemand verwendet jede Sprach- / Bibliotheksfunktion und
  2. Ein Wechsel des Verhaltens hier wäre aber nicht ratsam

Es ist völlig konsistent, an beiden oben genannten Merkmalen festzuhalten und noch einen weiteren Punkt hervorzuheben:

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

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

Es mag wahr sein, dass das Wechseln von Pferden im Mittelstrom hier zu einem erheblichen Bruch führen würde und dass es weitere Probleme geben könnte, wenn Python so geändert wird, dass es intuitiv mit Stefanos Eröffnungsschnipsel umgeht. Und es kann wahr sein, dass jemand, der Python-Interna gut kannte, ein Minenfeld von Konsequenzen erklären könnte. Jedoch,

Das vorhandene Verhalten ist nicht pythonisch, und Python ist erfolgreich, da nur sehr wenig über die Sprache das Prinzip des geringsten Erstaunens verletzt Näheso schlecht. Es ist ein echtes Problem, ob es sinnvoll wäre, es zu entwurzeln oder nicht. Es ist ein Designfehler. Wenn Sie die Sprache viel besser verstehen, indem Sie versuchen, das Verhalten aufzuspüren, kann ich sagen, dass C ++ all dies und mehr tut. Sie lernen viel, indem Sie beispielsweise durch subtile Zeigerfehler navigieren. Dies ist jedoch nicht Pythonic: Menschen, die sich genug für Python interessieren, um angesichts dieses Verhaltens durchzuhalten, sind Menschen, die von der Sprache angezogen werden, weil Python weit weniger Überraschungen als andere Sprachen hat. Dabbler und Neugierige werden zu Pythonisten, wenn sie erstaunt sind, wie wenig Zeit benötigt wird, um etwas zum Laufen zu bringen - nicht wegen eines Design-Fl - ich meine, versteckten Logik-Puzzles -, das gegen die Intuitionen von Programmierern verstößt, die sich für Python interessieren weil es einfach funktioniert .


6
-1 Obwohl dies eine vertretbare Perspektive ist, ist dies keine Antwort, und ich bin damit nicht einverstanden. Zu viele spezielle Ausnahmen erzeugen ihre eigenen Eckfälle.
Marcin

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

3
Und es ist unwissend, es als unglückliche Redewendung zu betrachten, ein Standardargument auf None zu setzen und dann im Hauptteil der Funktionseinstellung, wenn argument == None: argument = []? Ist es unwissend, diese Redewendung als unglücklich zu betrachten, da die Leute oft wollen, was ein naiver Neuling erwarten würde, dass wenn Sie f (argument = []) zuweisen, das Argument automatisch auf den Wert [] gesetzt wird?
Christos Hayward

3
Aber in Python gehört es zum Geist der Sprache, dass Sie nicht zu viele tiefe Tauchgänge machen müssen. array.sort () funktioniert und funktioniert unabhängig davon, wie wenig Sie über Sortierung, Big-O und Konstanten wissen. Das Schöne an Python im Array-Sortiermechanismus, um nur eines von unzähligen Beispielen zu nennen, ist, dass Sie nicht tief in die Interna eintauchen müssen. Und anders ausgedrückt, das Schöne an Python ist, dass man normalerweise nicht tief in die Implementierung eintauchen muss, um etwas zu erhalten, das einfach funktioniert. Und es gibt eine Problemumgehung (... if argument == None: argument = []), FAIL.
Christos Hayward

3
Als eigenständige Anweisung x=[]bedeutet die Anweisung "Erstellen Sie ein leeres Listenobjekt und binden Sie den Namen 'x' daran." In def f(x=[])wird also auch eine leere Liste erstellt. Es wird nicht immer an x ​​gebunden, sondern an den Standardersatz. Später, wenn f () aufgerufen wird, wird die Standardeinstellung herausgezogen und an x ​​gebunden. Da es die leere Liste selbst war, die entfernt wurde, ist dieselbe Liste das einzige, was an x ​​gebunden werden kann, unabhängig davon, ob etwas darin stecken geblieben ist oder nicht. Wie könnte es anders sein?
Jerry B

10

Dies ist kein Konstruktionsfehler . Wer darüber stolpert, macht etwas falsch.

Ich sehe 3 Fälle, in denen Sie auf dieses Problem stoßen könnten:

  1. Sie beabsichtigen, das Argument als Nebeneffekt der Funktion zu ändern. In diesem Fall ist es niemals sinnvoll , ein Standardargument zu haben. Die einzige Ausnahme ist, wenn Sie die Argumentliste missbrauchen, um beispielsweise Funktionsattribute zu haben cache={}, und von Ihnen nicht erwartet wird, dass Sie die Funktion mit einem tatsächlichen Argument aufrufen.
  2. Sie beabsichtigen , das Argument unmodifizierten zu verlassen, aber Sie versehentlich haben es zu ändern. Das ist ein Fehler, behebe ihn.
  3. Sie wollten das Argument für die Verwendung innerhalb der Funktion ändern, haben jedoch nicht erwartet, dass die Änderung außerhalb der Funktion angezeigt werden kann. In diesem Fall müssen Sie eine Kopie des Arguments erstellen, unabhängig davon, ob es die Standardeinstellung war oder nicht! Python ist keine Call-by-Value-Sprache, daher wird die Kopie nicht für Sie erstellt. Sie müssen dies explizit angeben.

Das Beispiel in der Frage könnte in Kategorie 1 oder 3 fallen. Es ist seltsam, dass sowohl die übergebene Liste geändert als auch zurückgegeben wird. Sie sollten den einen oder anderen auswählen.


"Etwas falsch machen" ist die Diagnose. Das heißt, ich denke, es gibt Zeiten, in denen = Kein Muster nützlich ist, aber im Allgemeinen möchten Sie nicht ändern, wenn in diesem Fall eine veränderbare Variable übergeben wird (2). Das cache={}Muster ist wirklich eine reine Interviewlösung, in echtem Code, den Sie wahrscheinlich wollen @lru_cache!
Andy Hayden

9

Dieser "Fehler" gab mir viele Überstunden! Aber ich fange an, eine mögliche Verwendung davon zu sehen (aber ich hätte es mir gewünscht, dass es immer noch zur Ausführungszeit ist)

Ich werde Ihnen das geben, was ich als nützliches Beispiel sehe.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

druckt Folgendes aus

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

Ändern Sie einfach die Funktion wie folgt:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

Ich denke, die Antwort auf diese Frage liegt darin, wie Python Daten an Parameter übergibt (Übergabe nach Wert oder Referenz), nicht nach Veränderbarkeit oder wie Python mit der Anweisung "def" umgeht.

Eine kurze Einführung. Erstens gibt es in Python zwei Arten von Datentypen, einen einfachen elementaren Datentyp wie Zahlen und einen anderen Datentyp Objekte. Zweitens übergibt Python beim Übergeben von Daten an Parameter den elementaren Datentyp nach Wert, dh erstellt eine lokale Kopie des Werts an eine lokale Variable, übergibt jedoch das Objekt als Referenz, dh Zeiger auf das Objekt.

Lassen Sie uns unter Berücksichtigung der beiden oben genannten Punkte erklären, was mit dem Python-Code passiert ist. Dies liegt nur daran, dass Objekte als Referenz übergeben werden, hat aber nichts mit veränderlichen / unveränderlichen oder wohl der Tatsache zu tun, dass die Anweisung "def" nur einmal ausgeführt wird, wenn sie definiert ist.

[] ist ein Objekt, daher übergibt Python die Referenz von [] an a, dh es aist nur ein Zeiger auf [], der als Objekt im Speicher liegt. Es gibt nur eine Kopie von [] mit vielen Verweisen darauf. Für das erste foo () wird die Liste [] durch die Append-Methode in 1 geändert . Beachten Sie jedoch, dass es nur eine Kopie des Listenobjekts gibt und dieses Objekt jetzt zu 1 wird . Beim Ausführen des zweiten foo () ist die Aussage der effbot-Webseite (Elemente werden nicht mehr ausgewertet) falsch. awird als Listenobjekt ausgewertet, obwohl der Inhalt des Objekts jetzt 1 ist . Dies ist der Effekt der Referenzübergabe! Das Ergebnis von foo (3) kann auf die gleiche Weise leicht abgeleitet werden.

Um meine Antwort weiter zu validieren, werfen wir einen Blick auf zwei zusätzliche Codes.

====== 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]

[]ist ein Objekt, so ist es None(das erstere ist veränderlich, während das letztere unveränderlich ist. Aber die Veränderlichkeit hat nichts mit der Frage zu tun). Keiner ist irgendwo im Raum, aber wir wissen, dass er da ist und es gibt nur eine Kopie von Keiner dort. Jedes Mal, wenn foo aufgerufen wird, werden Elemente bewertet (im Gegensatz zu einer Antwort, die nur einmal ausgewertet wird), um Keine zu sein, um klar zu sein, die Referenz (oder die Adresse) von Keine. Dann wird im foo das Element in [] geändert, dh 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]

Durch den Aufruf von foo (1) verweisen Elemente auf ein Listenobjekt [] mit einer Adresse, z. B. 11111111. Der Inhalt der Liste wird in der foo-Funktion in der Folge auf 1 geändert, die Adresse wird jedoch nicht geändert, immer noch 11111111 Dann kommt foo (2, []). Obwohl das [] in foo (2, []) beim Aufrufen von foo (1) den gleichen Inhalt wie der Standardparameter [] hat, sind ihre Adressen unterschiedlich! Da wir den Parameter explizit angeben, itemsmuss die Adresse dieses neuen übernommen werden[] angeben , z. B. 2222222, übernommen und nach einigen Änderungen zurückgegeben werden. Jetzt wird foo (3) ausgeführt. seit Nurxbereitgestellt wird, müssen Elemente wieder ihren Standardwert annehmen. Was ist der Standardwert? Sie wird beim Definieren der Funktion foo festgelegt: das in 11111111 befindliche Listenobjekt. Die Elemente werden also als Adresse 11111111 mit einem Element 1 ausgewertet. Die Liste in 2222222 enthält ebenfalls ein Element 2, wird jedoch nicht von Elementen angezeigt Mehr. Folglich ergibt ein Anhang von 3 items[1,3].

Aus den obigen Erklärungen können wir ersehen, dass die in der akzeptierten Antwort empfohlene Effbot- Webseite keine relevante Antwort auf diese Frage gab. Darüber hinaus denke ich, dass ein Punkt auf der Effbot-Webseite falsch ist. Ich denke, der Code bezüglich der Benutzeroberfläche ist korrekt:

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

Jede Taste kann eine eigene Rückruffunktion enthalten, die einen anderen Wert von anzeigt i. Ich kann ein Beispiel geben, um dies zu zeigen:

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

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


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

2
"Python-Pass-Elementardatentyp nach Wert, dh eine lokale Kopie des Werts in eine lokale Variable erstellen" ist völlig falsch. Ich bin erstaunt, dass jemand Python offensichtlich sehr gut kennen kann, aber solch ein schreckliches Missverständnis der Grundlagen hat. :-(
Veky

6

TLDR: Standardeinstellungen für die Definitionszeit sind konsistent und streng aussagekräftiger.


Das Definieren einer Funktion wirkt sich auf zwei Bereiche aus: den definierenden Bereich , der die Funktion enthält, und den Ausführungsbereich , der in der Funktion enthalten ist. Während es ziemlich klar ist, wie Blöcke Bereichen zugeordnet werden, ist die Frage, wo sie def <name>(<args=defaults>):hingehören:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

Das def nameTeil muss im definierenden Bereich ausgewertet werden - wir wollen nameschließlich dort verfügbar sein. Wenn die Funktion nur in sich selbst ausgewertet wird, ist sie nicht mehr zugänglich.

Da parameteres sich um einen konstanten Namen handelt, können wir ihn gleichzeitig mit "auswerten" def name. Dies hat auch den Vorteil, dass die Funktion mit einer bekannten Signatur name(parameter=...):anstelle einer bloßen Signatur erzeugt wird name(...):.

Wann ist zu bewerten default?

Konsistenz sagt bereits "bei Definition": Alles andere von def <name>(<args=defaults>):wird am besten auch bei Definition bewertet. Teile davon zu verzögern wäre die erstaunliche Wahl.

Die beiden Auswahlmöglichkeiten sind auch nicht gleichwertig: Wenn sie defaultzur Definitionszeit ausgewertet werden, kann dies immer noch die Ausführungszeit beeinflussen. Wenn defaultes zur Ausführungszeit ausgewertet wird, kann es die Definitionszeit nicht beeinflussen. Wenn Sie "bei Definition" auswählen, können beide Fälle ausgedrückt werden, während bei Auswahl von "bei Ausführung" nur einer ausgedrückt werden kann:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

"Konsistenz sagt bereits" bei Definition ": Alles andere von def <name>(<args=defaults>):wird auch bei Definition am besten bewertet." Ich glaube nicht, dass die Schlussfolgerung aus der Prämisse folgt. Nur weil zwei Dinge in derselben Zeile stehen, heißt das nicht, dass sie im selben Bereich bewertet werden sollten. defaultist etwas anderes als der Rest der Zeile: Es ist ein Ausdruck. Das Auswerten eines Ausdrucks unterscheidet sich stark vom Definieren einer Funktion.
LarsH

@ LarsH Funktionsdefinitionen werden in Python ausgewertet. Ob dies aus einer Anweisung ( def) oder einem Ausdruck ( lambda) stammt, ändert nichts daran, dass das Erstellen einer Funktion eine Bewertung bedeutet - insbesondere der Signatur. Die Standardeinstellungen sind Teil der Signatur einer Funktion. Das bedeutet nicht, dass die Standardeinstellungen sofort ausgewertet werden müssen - Typhinweise beispielsweise möglicherweise nicht. Aber es schlägt sicherlich vor, dass sie es tun sollten, es sei denn, es gibt einen guten Grund, dies nicht zu tun.
MisterMiyagi

OK, das Erstellen einer Funktion bedeutet in gewissem Sinne eine Bewertung, aber offensichtlich nicht in dem Sinne, dass jeder darin enthaltene Ausdruck zum Zeitpunkt der Definition bewertet wird. Die meisten sind es nicht. Mir ist nicht klar, in welchem ​​Sinne die Signatur zum Zeitpunkt der Definition besonders "ausgewertet" wird, ebenso wenig wie der Funktionskörper "ausgewertet" wird (in eine geeignete Darstellung zerlegt); wohingegen Ausdrücke im Funktionskörper eindeutig nicht im vollen Sinne bewertet werden. Unter diesem Gesichtspunkt würde die Konsistenz bedeuten, dass Ausdrücke in der Signatur auch nicht "vollständig" ausgewertet werden sollten.
LarsH

Ich meine nicht, dass Sie falsch liegen, nur, dass Ihre Schlussfolgerung nicht allein aus der Konsistenz folgt.
LarsH

@LarsH Standardwerte sind weder Teil des Körpers, noch behaupte ich, dass Konsistenz das einzige Kriterium ist. Können Sie einen Vorschlag machen, wie Sie die Antwort klären können?
MisterMiyagi

3

Jede andere Antwort erklärt, warum dies eigentlich ein nettes und gewünschtes Verhalten ist oder warum Sie dies sowieso nicht brauchen sollten. Meins ist für diejenigen, die hartnäckig sind und ihr Recht ausüben wollen, die Sprache nach ihrem Willen zu beugen, nicht umgekehrt.

Wir werden dieses Verhalten mit einem Dekorateur "beheben", der den Standardwert kopiert, anstatt dieselbe Instanz für jedes Positionsargument wiederzuverwenden, das auf seinem Standardwert belassen wird.

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Definieren wir nun unsere Funktion mit diesem Dekorator neu:

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

foo() # '[5]'
foo() # '[5]' -- as desired

Dies ist besonders praktisch für Funktionen, die mehrere Argumente annehmen. Vergleichen Sie:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

mit

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Es ist wichtig zu beachten, dass die obige Lösung nicht funktioniert, wenn Sie versuchen, Keyword-Argumente wie folgt zu verwenden:

foo(a=[4])

Der Dekorateur könnte angepasst werden, um dies zu ermöglichen, aber wir überlassen dies dem Leser als Übung;)

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.