python - umstellen - claws mail einrichten




"Wenig Erstaunen" und das veränderliche Standardargument (20)

Jeder, der lange genug an Python bastelt, wurde durch folgendes Problem gebissen (oder in Stücke gerissen):

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

Python-Novizen erwarten, dass diese Funktion immer eine Liste mit nur einem Element zurückgibt: [5] . Das Ergebnis ist stattdessen sehr unterschiedlich und für einen Anfänger sehr erstaunlich:

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

Ein Manager von mir hatte einmal seine erste Begegnung mit dieser Funktion und nannte sie "einen dramatischen Designfehler" der Sprache. Ich habe geantwortet, dass das Verhalten eine zugrunde liegende Erklärung hatte, und es ist in der Tat sehr rätselhaft und unerwartet, wenn Sie die Interna nicht verstehen. Ich konnte jedoch (mir) die folgende Frage 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 eigentlich statische Variablen in C verwendet, ohne Fehler zu erzeugen?)

Bearbeiten :

Baczek machte ein interessantes Beispiel. Zusammen mit den meisten Ihrer Kommentare und insbesondere mit 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 war, wo der Umfang der Parameter gesetzt werden sollte: innerhalb der Funktion oder "zusammen" damit?

Wenn die Bindung innerhalb der Funktion ausgeführt wird, bedeutet dies, dass x effektiv an den angegebenen Standard gebunden ist, wenn die Funktion aufgerufen und nicht definiert wird. Dies wäre ein schwerwiegender Fehler: Die def Linie wäre "hybrid" im Sinne dieses Teils der Bindung (des Funktionsobjekts) würde bei der Definition auftreten und Teil (Zuweisung von Standardparametern) zum Funktionsaufruf.

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


5 Punkte zur Verteidigung von Python

  1. Einfachheit : Das Verhalten ist in folgendem 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 der Funktionshauptteil). Es sollte daher zum Zeitpunkt des Moduls (und nur zum Zeitpunkt des Moduls, sofern es nicht verschachtelt ist) und nicht zum Funktionsaufruf ausgewertet werden.

  3. Nützlichkeit : Wie Frederik Lundh in seiner Erklärung der "Default-Parameterwerte in Python" erläutert, 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 laut "Wichtige Warnung" im ersten Unterabschnitt des Abschnitts "Weitere Informationen zum Definieren von Funktionen" angekündigt. Die Warnung verwendet sogar Fettdruck, der selten außerhalb von Überschriften angewendet wird. RTFM: Lesen Sie das feine Handbuch.

  5. Meta-Learning : In die Falle zu fallen ist in der Tat ein sehr hilfreicher Moment (zumindest wenn Sie ein reflektierender Lernender sind), da Sie später den oben genannten Punkt "Konsistenz" besser verstehen werden und der Sie viel über Python lernen wird.


Die Architektur

Das Zuweisen von Standardwerten in einem Funktionsaufruf ist ein Codegeruch.

def a(b=[]):
    pass

Dies ist eine Signatur einer Funktion, die nicht gut ist. Nicht nur wegen der von anderen Antworten beschriebenen Probleme. Ich werde hier nicht darauf eingehen.

Diese Funktion hat zwei Aufgaben. Erstellen Sie eine neue Liste und führen Sie eine Funktion aus, die sich höchstwahrscheinlich auf der Liste befindet.

Funktionen, die zwei Dinge bewirken, sind schlechte Funktionen, wie wir aus sauberen Codepraktiken lernen.

Wenn wir dieses Problem mit Polymorphismus angreifen, erweitern wir die Python-Liste oder wickeln eine in eine Klasse ein und führen dann unsere Funktion darauf aus.

Aber warte, sagst du, ich mag meine Einzeiler.

Rate mal. Code ist mehr als nur eine Möglichkeit, das Verhalten von Hardware zu steuern. Es ist eine Art von:

  • mit anderen Entwicklern kommunizieren und an demselben Code arbeiten.

  • das Verhalten der Hardware ändern können, wenn sich neue Anforderungen ergeben.

  • Sie können den Ablauf des Programms nach zwei Jahren erneut verstehen, um die oben erwähnte Änderung vorzunehmen.

Lassen Sie keine Zeitbomben, damit Sie sie später abholen können.

Wenn wir diese Funktion in zwei Dinge teilen, brauchen wir eine Klasse

class ListNeedsFives(object):
    def __init__(self, b=None):
        if b is None:
            b = []
        self.b = b

    def foo():
        self.b.append(5)

Ausgeführt von

a = ListNeedsFives()
a.foo()
a.b

Und warum ist das besser, als den gesamten Code zu einer einzigen Funktion zu mischen?

def dontdothis(b=None):
    if b is None:
        b = []
    b.append(5)
    return b

Warum machst du das nicht?

Wenn Sie in Ihrem Projekt nicht versagen, bleibt Ihr Code erhalten. Wahrscheinlich wird Ihre Funktion mehr als dies tun. Der richtige Weg, wartungsfähigen Code zu erstellen, besteht darin, Code mit einem entsprechend beschränkten Umfang in atomare Teile zu unterteilen.

Der Konstruktor einer Klasse ist eine allgemein anerkannte Komponente für jeden, der objektorientierte Programmierung durchgeführt hat. Durch das Platzieren der Logik, die die Listeninstanziierung im Konstruktor abwickelt, wird die kognitive Belastung des Verständnisses der Funktionen des Codes verringert.

Die Methode foo()gibt die Liste nicht zurück, warum nicht?

Wenn Sie eine eigenständige Liste zurückgeben, können Sie davon ausgehen, dass Sie das tun können, was auch immer Sie möchten. Es darf aber nicht sein, da es auch vom Objekt geteilt wird a. Wenn der Benutzer gezwungen wird, darauf zu verweisen, aberinnert er sich daran, wo die Liste hingehört. Jeder neue Code, der geändert abwerden soll, wird natürlich in die Klasse eingefügt, zu der er gehört.

Die def dontdothis(b=None):Signaturfunktion hat keine dieser Vorteile.


Warum betrachtest du dich nicht?

Ich bin wirklich überrascht, dass niemand die aufschlussreiche Introspektion von Python ( 2 und 3 ) auf Callables durchgeführt hat.

Gegeben eine einfache kleine Funktion, die wie func definiert ist:

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

Wenn Python darauf stößt, wird es zuerst kompiliert, um ein code Objekt für diese Funktion zu erstellen. Während dieser Kompilierungsschritt wird Python * ausgewertet und speichert dann die Standardargumente (hier eine leere Liste [] ) im Funktionsobjekt . Als oberste Antwort wurde erwähnt: Die Liste a kann nun als Mitglied der Funktion func .

Lassen Sie uns also eine Introspektion durchführen, ein Vorher und Nachher, um zu untersuchen, wie die Liste innerhalb des Funktionsobjekts erweitert wird. Ich verwende dafür Python 3.x , für Python 2 gilt dasselbe (verwenden Sie __defaults__ oder func_defaults in Python 2; ja, zwei Namen für dasselbe).

Funktion vor der Ausführung:

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

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

>>> func.__defaults__
([],)

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

Funktion nach Ausführung:

Lassen Sie uns nun diese Funktion ausführen:

>>> func()

Nun sehen wir uns diese __defaults__ einmal an:

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

Erstaunt? Der Wert innerhalb des Objekts ändert sich! Aufeinander folgende Aufrufe der Funktion werden jetzt einfach an dieses eingebettete list angehängt:

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

Also, da haben Sie es, der Grund, warum dieser 'Fehler' auftritt, ist, dass Standardargumente Teil des Funktionsobjekts sind. Hier ist nichts Unheimliches los, es ist alles nur ein bisschen überraschend.

Die häufigste Lösung, um dies zu bekämpfen, ist die Verwendung von None als Standard und die Initialisierung im Funktionshauptteil:

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 für a kein Argument übergeben wurde.

Um zu überprüfen, ob die Liste in __defaults__ der Liste in der Funktion func , können Sie einfach Ihre Funktion ändern, um die id der Liste zurückzugeben, die innerhalb des Funktionskörpers 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, wird input() aufgerufen, bevor die Funktion erstellt und an die bar gebunden wird.


AFAICS hat bisher noch keinen relevanten Teil der documentation :

Standardparameterwerte werden ausgewertet, wenn die Funktionsdefinition ausgeführt wird. Das 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 für das Verständnis, wenn ein Standardparameter ein veränderliches Objekt ist, beispielsweise 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 wirksam geändert. Dies ist im Allgemeinen nicht das, was beabsichtigt war. Eine Möglichkeit, dies zu umgehen, besteht darin, None als Standard zu verwenden und explizit im Rumpf der Funktion dafür zu testen [...]


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

  1. Die Deklaration der Funktion (Klasse usw.) wird nur einmal ausgeführt und erstellt alle Standardwertobjekte
  2. alles wird als Referenz übergeben

So:

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 changes - Vorgang wird für dasselbe Objekt ausgeführt - und es wird gedruckt

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

1. Leistung

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

Wenn die Anrufzeitauswertung verwendet wird, wird die teure Funktion jedes Mal aufgerufen, wenn Ihre Funktion ohne Argument verwendet wird. Sie zahlen entweder für jeden Anruf einen teuren Preis oder müssen den Wert manuell manuell zwischenspeichern, den Namespace verschmutzen und die Ausführlichkeit erhöhen.

2. Erzwingen gebundener Parameter

Ein nützlicher Trick besteht darin, die Parameter eines Lambda 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, binden sie i stattdessen an den Aufrufzeitwert von i, sodass Sie eine Liste von Funktionen erhalten, die alle zurückgegeben wurden 9 .

Die einzige Möglichkeit, dies anderweitig zu implementieren, wäre das Erstellen eines weiteren Abschlusses mit der i-Bindung, 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 zu den Argumenten und Standardwerten erhalten Sie mit dem Modul inspect

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

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

Nehmen wir an, das Verhalten der Standardwerte könnte geändert werden, sodass dies dem Äquivalent entspricht:

_undefined = object()  # sentinel value

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

Wir haben jedoch die Fähigkeit verloren, sich selbst zu untersuchen und zu sehen, was die Standardargumente sind . Da die Objekte noch nicht konstruiert wurden, können wir sie niemals erreichen, ohne die Funktion tatsächlich aufzurufen. Am besten speichern wir den Quellcode und geben diesen als String zurück.


Nun, der Grund liegt ganz einfach darin, dass Bindungen ausgeführt werden, wenn Code ausgeführt wird, und die Funktionsdefinition ausgeführt wird. Nun, 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 Ereignis. 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 "How It Works", und wenn Sie es im Funktionsfall anders machen, wäre dies wahrscheinlich kompliziert und im Klassenfall wahrscheinlich unmöglich oder zumindest verlangsamen Sie die Objekt-Instantiierung erheblich, da Sie den Klassencode beibehalten müssen und führen Sie es aus, wenn Objekte erstellt werden.

Ja, es ist unerwartet. Aber sobald der Cent gefallen ist, passt er perfekt zu der Funktionsweise von Python. Tatsächlich ist es eine gute Unterrichtshilfe, und wenn Sie erst verstanden haben, warum dies geschieht, werden Sie viel besser mit Python spielen können.

Es sollte jedoch in jedem guten Python-Tutorial eine herausragende Rolle spielen. Denn wie gesagt, jeder trifft früher oder später auf dieses Problem.


Tatsächlich ist dies kein Konstruktionsfehler, und es liegt nicht an internen Komponenten oder Leistung.
Das liegt einfach daran, dass Funktionen in Python erstklassige Objekte sind und nicht nur ein Stück Code.

Sobald Sie auf diese Art und Weise nachdenken, ist es durchaus sinnvoll: Eine Funktion ist ein Objekt, das nach seiner Definition bewertet wird. Standardparameter sind eine Art "Mitgliedsdaten" und daher kann sich ihr Status von einem Aufruf zum anderen ändern - genau wie in jedem anderen Objekt.

In jedem Fall hat Effbot eine sehr nette Erklärung für die Gründe für dieses Verhalten in Default Parameter Values ​​in Python .
Ich fand es sehr klar, und ich schlage vor, es zu lesen, um besser zu wissen, wie Funktionsobjekte funktionieren.


Ändern Sie einfach die Funktion wie folgt:

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

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

Ich sehe drei 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 für Funktionsattribute missbrauchen, z. B. cache={}, dass Sie die Funktion nicht 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, korrigieren Sie es.
  3. Sie möchten 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 machen.

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 das eine oder das andere auswählen.


Die Lösungen hier sind:

  1. Verwenden Sie diesen Noneals Standardwert (oder eine Nonce object) und aktivieren Sie ihn , um Ihre Werte zur Laufzeit zu erstellen. oder
  2. Verwenden Sie a lambdaals Standardparameter und rufen Sie es innerhalb eines try-Blocks auf, um den Standardwert zu erhalten (dies ist die Art von Sache, für die die Lambda-Abstraktion gedacht ist).

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


Dies hat eigentlich nichts mit Standardwerten zu tun, außer dass es oft zu einem unerwarteten Verhalten kommt, 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 vom Anrufer übergeben, wenn der Anrufer dies nicht erwarten. Code wie dieser wäre gut, wenn die Funktion so genannt würde append_5; Dann ruft der Aufrufer die Funktion auf, um den übergebenen Wert zu ändern, und das Verhalten wird erwartet. Es wäre jedoch sehr unwahrscheinlich, dass eine solche Funktion ein Standardargument akzeptiert und die Liste wahrscheinlich nicht zurückgibt (da der Anrufer bereits einen Verweis auf diese Liste hat; der, den er gerade übergeben hat).

Ihr Original foosollte mit einem Standardargument nicht ändern, aob es explizit übergeben wurde oder den Standardwert erhielt. Ihr Code sollte veränderbare Argumente allein lassen, sofern sich aus dem Kontext / Namen / der Dokumentation nicht ergibt, dass die Argumente geändert werden sollen. Die Verwendung von veränderlichen Werten, die als Argumente als lokale temporäre Variablen übergeben werden, ist eine äußerst schlechte Idee, unabhängig davon, ob wir uns in Python befinden oder nicht und ob es Standardargumente gibt oder nicht.

Wenn Sie ein lokales temporäres Element im Verlauf der Berechnung etwas destruktiv bearbeiten müssen und Ihre Manipulation mit einem Argumentwert beginnen muss, müssen Sie eine Kopie erstellen.


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 (gut erklärt in der akzeptierten Antwort).

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 . Versuchen Sie, einem schreibgeschützten Klassenattribut einen Wert zuzuweisen:

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

Schauen Sie sich das ursprüngliche Beispiel an 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 bei foo.func_defs[0]). Da aist eine Liste, aist veränderbar und ist somit ein Lese-Schreib-Attribut von foo. Es wird mit der durch die Signatur angegebenen leeren Liste initialisiert, wenn die Funktion instanziiert wird, und kann gelesen und geschrieben werden, solange das Funktionsobjekt vorhanden ist.

Beim Aufrufen fooohne Überschreiben eines Standardwerts wird der Wert dieses Standardwerts von verwendet foo.func_defs. In diesem Fall foo.func_defs[0]wird ainnerhalb des Funktionsbereichs des Funktionsobjekts verwendet. Änderungen an der aÄnderung foo.func_defs[0], die Teil des fooObjekts ist und zwischen der Ausführung des Codes in bleibt foo.

Vergleichen Sie dies nun mit dem Beispiel aus der Dokumentation zur Emulation des Standardverhaltens von Argumenten anderer Sprachen , sodass die Funktions-Signatur- Standardwerte bei jeder Ausführung der Funktion verwendet werden:

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

Wenn man (1) und (2) berücksichtigt, kann man sehen, warum dies das gewünschte Verhalten bewirkt:

  • Wenn das fooFunktionsobjekt instanziiert foo.func_defs[0]ist, Nonewird ein unveränderliches Objekt festgelegt.
  • Wenn die Funktion mit Standardwerten ausgeführt wird (wobei Lim Funktionsaufruf kein Parameter angegeben ist ), steht foo.func_defs[0]( None) im lokalen Bereich als zur Verfügung L.
  • Bei L = []kann die Zuweisung nicht erfolgreich sein foo.func_defs[0], da das Attribut schreibgeschützt ist.
  • Per (1) , eine neue lokale Variable auch genannt Lim lokalen Bereich erstellt wird und für den Rest des Funktionsaufrufes verwendet. foo.func_defs[0]bleibt somit für zukünftige Aufrufe von foo.

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]

Ich denke, die Antwort auf diese Frage liegt darin, wie Python Daten an Parameter übergibt (Wert oder Referenz übergeben), nicht Mutabilität oder wie Python die "def" -Anweisung behandelt.

Eine kurze Einführung Erstens gibt es zwei Arten von Datentypen in Python, einer ist ein einfacher elementarer Datentyp wie Zahlen und ein anderer Datentyp sind Objekte. Zweitens: Wenn Sie Daten an Parameter übergeben, übergeben Python elementaren Datentypen nach Wert, dh erstellen Sie eine lokale Kopie des Werts in eine lokale Variable, übergeben Sie jedoch Objekt als Referenz, dh Zeiger auf das Objekt.

Wenn wir die beiden oben genannten Punkte zugeben, wollen wir erklären, was mit dem Python-Code passiert ist. Dies ist nur auf das Übergeben von Verweisen für Objekte zurückzuführen, hat aber nichts mit mutable / unverändelbar zu tun oder die Tatsache, dass die "def" -Anweisung nur einmal ausgeführt wird, wenn sie definiert ist.

[] ist ein Objekt, also ü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 . Wenn das zweite foo () ausgeführt wird, ist das, was die Effbot-Webseite sagt (Elemente werden nicht mehr ausgewertet), falsch. awird als Listenobjekt ausgewertet, obwohl der Inhalt des Objekts jetzt 1 ist . Dies ist der Effekt von Referenzübergabe! Das Ergebnis von foo (3) kann auf dieselbe Weise leicht abgeleitet werden.

Um meine Antwort weiter zu bestätigen, betrachten wir 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, also 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 es dort ist, und es gibt nur eine Kopie von None. Jedes Mal, wenn foo aufgerufen wird, werden Elemente (im Gegensatz zu einer Antwort, dass sie nur ein einziges Mal ausgewertet wird) als None bewertet, um klar zu sein, die Referenz (oder die Adresse) von None. Im Foo wird dann item 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]

Der Aufruf von foo (1) bewirkt, dass Elemente auf ein Listenobjekt [] mit einer Adresse, z. B. 11111111, zeigen. Der Inhalt der Liste wird in der foo-Funktion in der Folge auf 1 geändert, die Adresse wird jedoch nicht geändert, noch 11111111 Dann kommt foo (2, []). [] In foo (2, []) hat zwar den gleichen Inhalt wie der Standardparameter [], wenn foo (1) aufgerufen wird, ihre Adresse ist jedoch unterschiedlich! Da wir den Parameter explizit angeben , itemsmuss die Adresse dieses Neuen [], z. B. 2222222, übernommen und nach einer Änderung zurückgegeben werden. Jetzt wird foo (3) ausgeführt. seit Nurxangegeben wird, muss items seinen Standardwert erneut annehmen. Was ist der Standardwert? Es wird festgelegt, wenn die Funktion foo definiert wird: das Listenobjekt in 11111111. Die Elemente werden daher als Adresse 11111111 mit einem Element 1 ausgewertet. Die Liste bei 2222222 enthält auch ein Element 2, auf das jedoch keine Elemente verweisen Mehr. Ein Anhang von 3 macht also items[1,3].

Aus den obigen Erläuterungen ist ersichtlich, dass die in der akzeptierten Antwort empfohlene effbot- Webseite keine relevante Antwort auf diese Frage gab. Ich finde, ein Punkt auf der Effbot-Webseite ist falsch. Ich denke, dass der Code bezüglich der UI.Button korrekt ist:

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 unterschiedliche Werte 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 anderen Wert von i.


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

Da andere ausführlich kommentiert wurden, ist der Listenparameter an die Funktion gebunden, wenn sie definiert ist, im Gegensatz zur Ausführung. Da Listen und Wörterbücher veränderbar sind, wirken sich Änderungen an diesem Parameter auf andere Aufrufe dieser Funktion aus. Folglich erhalten nachfolgende Aufrufe der Funktion diese freigegebene Liste, die möglicherweise durch andere Aufrufe der Funktion geändert wurde. Noch schlimmer ist, dass zwei Parameter gleichzeitig den gemeinsam genutzten Parameter dieser Funktion verwenden und die Änderungen der anderen nicht berücksichtigen.

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 sie dasselbe Objekt sind, indem Sie Folgendes verwenden id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Pro Brett Slatkins "Effektiver Python: 59 spezifische Methoden zum Schreiben von besserem Python", Punkt 20: Verwenden Sie Noneund Docstrings, um dynamische Standardargumente anzugeben (S. 48).

Die Konvention zum Erzielen des gewünschten Ergebnisses in Python besteht darin, einen Standardwert für Nonedas tatsächliche Verhalten im Dokumentstring anzugeben und dieses zu dokumentieren.

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

Bevorzugte Methode :

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

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

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

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

Es kann legitime Anwendungsfälle für die 'falsche Methode' geben, wobei der Programmierer die Freigabe des Standardlistenparameters beabsichtigt, dies ist jedoch eher die Ausnahme als die Regel.


Wenn wir das tun:

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

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

Um es für diese Diskussion einfacher zu machen, 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 er aist, verwenden wir ihn jederzeit wieder pavlo.

Wenn pavloes veränderbar (modifizierbar) ist und fooam Ende geändert wird, wird ein Effekt, den wir beim nächsten Mal bemerken foo, ohne Angabe aufgerufen a.

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

 >>> foo()
 [5]

Nun pavloist es [5].

Ein foo()erneuter Aufruf ändert sich pavloerneut:

>>> foo()
[5, 5]

Festlegen, awann der Anruf foo()sicherstellt, pavlowird nicht berührt.

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

So pavloist es immer noch [5, 5].

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

1) Die so genannte Problem der „Veränderliche Standard Argument“ im Allgemeinen ein spezielles Beispiel ist das demonstriert:
„Alle Funktionen mit diesem Problem leiden auch unter ähnlichen Nebeneffekt Problem auf dem tatsächlichen Parameter
Das ist gegen die Regeln der funktionalen Programmierung, normalerweise unerwünscht und sollte beides zusammen fixiert 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 ist das copyoder deepcopydas Eingabeobjekt zuerst und dann das, was mit der Kopie gemacht wird.

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

Viele eingebaute, veränderliche Typen haben eine Kopiermethode wie some_dict.copy()oder some_set.copy()oder können einfach wie somelist[:]oder kopiert werden list(some_list). Jedes Objekt kann auch durch copy.copy(any_object)oder genauer durch copy.deepcopy()(das letztere ist nützlich, wenn das veränderliche Objekt aus veränderlichen Objekten besteht) kopiert werden . Einige Objekte basieren im Wesentlichen auf Nebeneffekten wie "Datei" -Objekten und können durch Kopieren nicht sinnvoll wiedergegeben werden. copying

Beispielproblem für eine ähnliche SO-Frage

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

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

Es sollte auch nicht in einem öffentlichen Attribut einer von dieser Funktion zurückgegebenen Instanz gespeichert werden. (Angenommen, private Attribute einer Instanz sollten nicht durch Konventionen außerhalb dieser Klasse oder Unterklassen geändert werden, dh es _var1handelt sich um ein privates Attribut.)

Schlussfolgerung:
Eingabeparameterobjekte sollten nicht an ihrer Stelle geändert (mutiert) oder in ein von der Funktion zurückgegebenes Objekt eingebunden werden. (Wenn wir die Programmierung ohne Nebenwirkungen bevorzugen, wird dies dringend empfohlen. Weitere Informationen finden Sie im Wiki unter "Nebenwirkungen". (Die ersten beiden Absätze sind in diesem Zusammenhang relevant.)

2)
Nur wenn der Nebeneffekt des Aktualparameters erforderlich ist, der Standardparameter jedoch unerwünscht ist, ist die nützliche Lösung def ...(var1=None): if var1 is None: var1 = [] More..

3) In einigen Fällen ist das veränderbare Verhalten von Standardparametern hilfreich .


Dieser "Fehler" hat mir viele Überstunden geleistet! Aber ich fange an, eine mögliche Verwendung davon zu sehen (aber ich hätte es gerne noch zur Ausführungszeit gehabt)

Ich gebe dir das, 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 das Folgende

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

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 eigentlich statische Variablen in C verwendet, ohne Fehler zu erzeugen?)

Wie Sie sehen, gibt es einen Leistungsvorteil, wenn Sie unveränderliche Standardargumente verwenden. 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 gibt es Konstanten, die ziemlich frei sind. In Python haben Sie diesen Vorteil nicht.







least-astonishment