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 x
es effektiv an den angegebenen Standard gebunden ist, wenn die Funktion aufgerufen und nicht definiert wird, was einen tiefen Fehler darstellen würde: Die def
Linie 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.
quelle
[5]
" Ich bin ein Python - Neuling, und ich würde dies nicht erwarten, denn offensichtlichfoo([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]
.Antworten:
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.
quelle
functions are objects
. In Ihrem Paradigma besteht der Vorschlag darin, die Standardwerte der Funktionen als Eigenschaften und nicht als Attribute zu implementieren.Angenommen, Sie haben den folgenden Code
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
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
foo
obige 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:
Verwendet meine Karte nun den Wert des
StringBuffer
Schlü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 derMap
Verwendung 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.
quelle
("blueberries", "mangos")
.some_random_function()
Appends zufruits
ihm statt der Zuordnung das Verhalteneat()
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.global
das Tupel nur explizit deklariert und neu zugewiesen - es ist absolut nicht überraschend, wenn es danacheat
anders funktioniert.Der relevante Teil der Dokumentation :
quelle
function.data = []
) einzufügen oder noch besser ein Objekt zu erstellen.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:
Sie erwarten eine neue Liste, auf die verwiesen wird
a
.Warum sollte das
a=[]
ineine 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:
Benutzer, möchten Sie
a
standardmäßig die Datums- und Uhrzeitangabe festlegen, die der Definition oder Ausführung entsprichtx
? 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 Funktionsaufrufdatetime.now()
aufgerufen). Wenn der Benutzer andererseits die Definition-Zeit-Zuordnung wünschte, konnte er schreiben:Ich weiß, ich weiß: das ist eine Schließung. Alternativ kann Python ein Schlüsselwort bereitstellen, um die Bindung der Definitionszeit zu erzwingen:
quelle
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).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:
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.
quelle
property
s 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 = value
wie Lennart sagte, verwenden).Warum siehst du nicht nach innen?
Ich bin wirklich überrascht, dass niemand die aufschlussreiche Selbstbeobachtung durchgeführt hat, die Python (
2
und die3
Anwendung) für Callables bietet .Gegeben eine einfache kleine Funktion
func
definiert als:Wenn Python darauf stößt, wird es zunächst kompiliert, um ein
code
Objekt 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 Listea
kann nun als Mitglied der Funktion betrachtet werdenfunc
.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.x
dafür, für Python 2 gilt das Gleiche (benutze__defaults__
oderfunc_defaults
in Python 2; ja, zwei Namen für das gleiche Ding).Funktion vor der Ausführung:
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: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:
Nun wollen wir diese noch
__defaults__
einmal sehen:Erstaunt? Der Wert im Objekt ändert sich! Aufeinanderfolgende Aufrufe der Funktion werden jetzt einfach an das eingebettete
list
Objekt angehängt :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,
None
als Standard zu verwenden und dann im Funktionskörper zu initialisieren: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 verwendetenfunc
übereinstimmt, können Sie einfach Ihre Funktion ändern, umid
die Liste zurückzugeben,a
die 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: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:
Wie Sie feststellen werden,
input()
wird es aufgerufen, bevor die Funktion erstellt und an den Namen gebundenbar
wird.quelle
id(...)
für diese letzte Überprüfung benötigt oder würde deris
Bediener dieselbe Frage beantworten?is
würde gut funktionieren, ich habe es nur verwendet,id(val)
weil ich denke, dass es intuitiver sein könnte.None
als 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.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
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:
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
i
an den Aufrufzeitwert von i gebunden, sodass Sie eine Liste der Funktionen erhalten, die alle zurückgegeben wurden9
.Die einzige Möglichkeit, dies anderweitig zu implementieren, besteht darin, einen weiteren Abschluss mit der i-Grenze zu erstellen, dh:
3. Selbstbeobachtung
Betrachten Sie den Code:
Informationen über die Argumente und Standardeinstellungen können wir mit dem
inspect
Modul abrufenDiese 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:
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.
quelle
5 Punkte zur Verteidigung von Python
Einfachheit : Das Verhalten ist im folgenden Sinne einfach: Die meisten Menschen geraten nur einmal in diese Falle, nicht mehrmals.
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.
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.)
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.
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.
quelle
Dieses Verhalten lässt sich leicht erklären durch:
Damit:
a
ändert sich nicht - jeder Zuweisungsaufruf erstellt ein neues int-Objekt - ein neues Objekt wird gedrucktb
ändert sich nicht - neues Array wird aus dem Standardwert erstellt und gedrucktc
Änderungen - Operation wird an demselben Objekt ausgeführt - und es wird gedrucktquelle
__iadd__
, aber es funktioniert nicht mit int. Na sicher. :-)Was Sie fragen, ist, warum dies:
ist intern nicht gleichbedeutend damit:
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.
quelle
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:
Lösung : eine Kopie
Eine absolut sichere Lösung besteht darin, zuerst das Eingabeobjekt
copy
oderdeepcopy
das Eingabeobjekt zu erstellen und dann alles mit der Kopie zu tun.Viele eingebaute veränderbare Typen haben eine Kopiermethode wie
some_dict.copy()
odersome_set.copy()
oder können einfach wiesomelist[:]
oder kopiert werdenlist(some_list)
. Jedes Objekt kann auch voncopy.copy(any_object)
oder gründlicher von kopiert werdencopy.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. KopierenBeispielproblem für eine ähnliche SO-Frage
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
_var1
ist 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 .
quelle
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 diesif a is None: a = [1, 2, 3]
tun, kopieren Sie die Liste trotzdem.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.
In diesem Code sind keine Standardwerte in Sicht, aber Sie erhalten genau das gleiche Problem.
Das Problem ist , dass
foo
sich 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
foo
mit einem Standardargument sollte nicht ändern,a
ob 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.
quelle
append
, dass es sicha
"an Ort und Stelle" ändert ). Dass ein Standard-Mutable nicht bei jedem Aufruf erneut instanziiert wird, ist das "unerwartete" Bit ... zumindest für mich. :)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).cache={}
Vollständigkeit halber möchte ich Ihr Beispiel hinzufügen .None
und Zuweisung des tatsächlichen Standards, wenn das Argument aktiviert istNone
, 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.Bereits beschäftigtes Thema, aber nach dem, was ich hier gelesen habe, hat mir Folgendes geholfen zu erkennen, wie es intern funktioniert:
quelle
a = a + [1]
überlastet sind.a
Erwägen Sie, es zu ändernb = a + [1] ; print id(b)
und eine Zeile hinzuzufügena.append(2)
. Dadurch wird deutlicher, dass+
auf zwei Listen immer eine neue Liste (zugewiesenb
) erstellt wird, während eine geänderte Listea
immer noch dieselbe haben kannid(a)
.Es ist eine Leistungsoptimierung. Welcher dieser beiden Funktionsaufrufe ist Ihrer Meinung nach aufgrund dieser Funktionalität schneller?
Ich gebe dir einen Hinweis. Hier ist die Demontage (siehe http://docs.python.org/library/dis.html ):
#
1#
2Wie 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.
quelle
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:
ist fast genau gleichbedeutend damit:
Demonstration
Hier ist eine Demonstration: Sie können jedes Mal, wenn auf sie verwiesen wird, überprüfen, ob es sich um dasselbe Objekt handelt
example.py
und laufen lassen mit
python example.py
: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:
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:
quelle
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:
Hoffentlich reicht es zu zeigen, dass
def
es 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.
quelle
Eine einfache Problemumgehung mit None
quelle
Dieses Verhalten ist nicht überraschend, wenn Sie Folgendes berücksichtigen:
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:
Schauen Sie zurück zum ursprünglichen Beispiel und beachten Sie die obigen Punkte:
Hier
foo
ist ein Objekt unda
ist ein Attribut vonfoo
(verfügbar unterfoo.func_defs[0]
). Daa
ist eine Liste,a
ist veränderlich und ist somit ein Lese- / Schreibattribut vonfoo
. 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
foo
ohne Überschreiben eines Standards wird der Wert dieses Standards von verwendetfoo.func_defs
. In diesem Fallfoo.func_defs[0]
wirda
innerhalb des Codebereichs des Funktionsobjekts verwendet. Änderungen ana
Änderungenfoo.func_defs[0]
, die Teil desfoo
Objekts sind und zwischen der Ausführung des Codes in bestehen bleibenfoo
.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:
Unter Berücksichtigung von (1) und (2) kann man sehen, warum dies das gewünschte Verhalten bewirkt:
foo
Funktionsobjekt instanziiert wird,foo.func_defs[0]
wird es aufNone
ein unveränderliches Objekt gesetzt.L
im Funktionsaufruf ein Parameter angegeben wurde ), istfoo.func_defs[0]
(None
) im lokalen Bereich als verfügbarL
.L = []
kann die Zuweisung nicht erfolgreich seinfoo.func_defs[0]
, da dieses Attribut schreibgeschützt ist.L
im lokalen Bereich erstellt wird , und für den Rest des Funktionsaufrufes.foo.func_defs[0]
bleibt somit für zukünftige Aufrufe von unverändertfoo
.quelle
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 ...) :
Sie können überprüfen, ob es sich um ein und dasselbe Objekt handelt, indem Sie Folgendes verwenden
id
:Per Brett Slatkins "Effektives Python: 59 spezifische Möglichkeiten, besseres Python zu schreiben", Punkt 20: Verwenden Sie
None
und Docstrings, um dynamische Standardargumente anzugeben (S. 48).Diese Implementierung stellt sicher, dass jeder Aufruf der Funktion entweder die Standardliste oder die an die Funktion übergebene Liste empfängt.
Bevorzugte Methode :
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.
quelle
Die Lösungen hier sind:
None
als Standardwert (oder als Nonceobject
) und aktivieren Sie diesen Wert , um Ihre Werte zur Laufzeit zu erstellen. oderlambda
als 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
).quelle
Wenn wir das tun:
... ordnen wir das Argument
a
einer 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
?Wenn der Anrufer uns nicht sagt, was
a
ist, können wir jederzeit wiederverwendenpavlo
.Wenn
pavlo
es veränderbar (modifizierbar) ist und esfoo
letztendlich modifiziert, wird ein Effekt, den wir beim nächsten Mal bemerkenfoo
, ohne Angabe aufgerufena
.Das sehen Sie also (Denken Sie daran,
pavlo
wird mit [] initialisiert):Nun
pavlo
ist [5].Erneutes Aufrufen
foo()
ändert sichpavlo
erneut:Die Angabe
a
beim Anrufenfoo()
stellt sicher, dasspavlo
nicht berührt wird.Also
pavlo
ist es immer noch[5, 5]
.quelle
Ich nutze dieses Verhalten manchmal als Alternative zum folgenden Muster:
Wenn
singleton
nur von verwendet wirduse_singleton
, mag ich das folgende Muster als Ersatz: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.
quelle
_make_singleton
im 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.Sie können dies umgehen, indem Sie das Objekt (und damit die Verbindung zum Zielfernrohr) ersetzen:
Hässlich, aber es funktioniert.
quelle
Es kann wahr sein, dass:
Es ist völlig konsistent, an beiden oben genannten Merkmalen festzuhalten und noch einen weiteren Punkt hervorzuheben:
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 .
quelle
x=[]
bedeutet die Anweisung "Erstellen Sie ein leeres Listenobjekt und binden Sie den Namen 'x' daran." Indef 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?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:
cache={}
, und von Ihnen nicht erwartet wird, dass Sie die Funktion mit einem tatsächlichen Argument aufrufen.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.
quelle
cache={}
Muster ist wirklich eine reine Interviewlösung, in echtem Code, den Sie wahrscheinlich wollen@lru_cache
!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.
druckt Folgendes aus
quelle
Ändern Sie einfach die Funktion wie folgt:
quelle
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 esa
ist 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.a
wird 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 ========
[]
ist ein Objekt, so ist esNone
(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 =======
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,
items
muss 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 Nurx
bereitgestellt 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 3items
[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:
Jede Taste kann eine eigene Rückruffunktion enthalten, die einen anderen Wert von anzeigt
i
. Ich kann ein Beispiel geben, um dies zu zeigen:Wenn wir ausführen, erhalten
x[7]()
wir wie erwartet 7 undx[9]()
geben 9, einen weiteren Wert voni
.quelle
x[7]()
ist9
.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:Das
def name
Teil muss im definierenden Bereich ausgewertet werden - wir wollenname
schließlich dort verfügbar sein. Wenn die Funktion nur in sich selbst ausgewertet wird, ist sie nicht mehr zugänglich.Da
parameter
es 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 Signaturname(parameter=...):
anstelle einer bloßen Signatur erzeugt wirdname(...):
.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
default
zur Definitionszeit ausgewertet werden, kann dies immer noch die Ausführungszeit beeinflussen. Wenndefault
es 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:quelle
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.default
ist etwas anderes als der Rest der Zeile: Es ist ein Ausdruck. Das Auswerten eines Ausdrucks unterscheidet sich stark vom Definieren einer Funktion.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.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.
Definieren wir nun unsere Funktion mit diesem Dekorator neu:
Dies ist besonders praktisch für Funktionen, die mehrere Argumente annehmen. Vergleichen Sie:
mit
Es ist wichtig zu beachten, dass die obige Lösung nicht funktioniert, wenn Sie versuchen, Keyword-Argumente wie folgt zu verwenden:
Der Dekorateur könnte angepasst werden, um dies zu ermöglichen, aber wir überlassen dies dem Leser als Übung;)
quelle