Verstehe ich nicht: lokale Variable wie global

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Donnerstag 26. August 2010, 21:58

Der Code, dessen Ergebnis ich nicht verstehe:

Code: Alles auswählen

def spam(a=True, b=[]):
    b.append("foo")
    b.append("bar")
    return b

def eggs(b=[]):
    return spam(False, b)

if __name__ == "__main__":
    print eggs()
    print eggs()
    print eggs()
    """ returns:
    ['foo', 'bar']
    ['foo', 'bar', 'foo', 'bar']
    ['foo', 'bar', 'foo', 'bar', 'foo', 'bar']
    """
    print eggs([])
    """ returns:
    ['foo', 'bar']
    """
Wieso füllt `eggs()`bei jedem Aufruf die Funktions-interne Variable `b` immer weiter mit Werten auf? Wenn ich diese nicht explizit als Argument an die Funktion übergebe, sollte der Wert doch immer eine leere Liste sein, richtig?

Erst wenn ich `eggs([])` aufrufe, also eine leere Liste als "Startwert" übergebe, dann klappt es wie ich es erwartet hätte.

Hab ich einen Denkfehler? Der Code verhält sich so, als sei `b` eine globale Variable.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Donnerstag 26. August 2010, 22:03

Hier mal anders, aber genau so seltsam:

Code: Alles auswählen

def spam(a=True, b=[]):
    b.append("foo")
    b.append("bar")
    return b

def eggs(b=[]):
    print 1, b
    return spam(False, b)

if __name__ == "__main__":
    x = eggs()
    print x
    x = eggs()
    print x
    b = [] # bringt nix, ist also nicht "global"
    x = eggs()
    print x
    """ returns:
    1 []
    ['foo', 'bar']
    1 ['foo', 'bar']
    ['foo', 'bar', 'foo', 'bar']
    1 ['foo', 'bar', 'foo', 'bar']
    ['foo', 'bar', 'foo', 'bar', 'foo', 'bar']
    """
    print eggs([])
    """ returns:
    1 []
    ['foo', 'bar']
    """
Man könnte auch sagen: `eggs` "merkt" sich den letzten Wert von `b` und verwendet diesen beim nächsten Aufruf anstelle des Initialwerts des Arguments, wenn dieses nicht explizit angegeben wurde.

P.S. Ich verwende Python 2.6 (r26:66721, Oct 2 2008, 11:35:03) [MSC v.1500 32 bit (Intel)] on win32.
Zuletzt geändert von droptix am Donnerstag 26. August 2010, 22:05, insgesamt 1-mal geändert.
Benutzeravatar
cofi
Moderator
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Donnerstag 26. August 2010, 22:03

Die Funktion wird eben nur einmal evaluiert, b hat immer _dieselbe_ Liste als Default (das ist auch sonst immer dasselbe Objekt). Das Phaenomen taucht aber nur dann auf, wenn man veraenderbare Default-Werte hat, also Listen, Dictionaries, ...

Code: Alles auswählen

def spam(a=True, b=None):
    if b is None:
        b = list()
    ...
Ist der "Standardweg".
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Samstag 28. August 2010, 16:06

Komisch, ich verstehe es trotzdem nicht ganz, aber naja. Gibt's einen Unterschied zu:

Code: Alles auswählen

def spam(a=True, b=None):
    if b is None:
        b = [] # statt `b = list()`
Oder ist das dann egal?
Benutzeravatar
DaMutz
User
Beiträge: 202
Registriert: Freitag 31. Oktober 2008, 17:25

Samstag 28. August 2010, 16:14

es ist egal:

Code: Alles auswählen

In [1]: list() == []
Out[1]: True
nur kann man list() besser lesen.
BlackJack

Samstag 28. August 2010, 17:10

@droptix: Die Ausdrücke bei Default-Argumenten werden *einmal* ausgewertet, und zwar wenn die Funktionsdefinition ausgeführt wird. Der Ausdruck wird *nicht* jedesmal ausgewertet wenn die Funktion aufgerufen wird. Das Funktionsobjekt merkt sich das Ergebnis bei der einen Auswertung bei der Definition und jedes mal wenn die Funktion ausgeführt wird, dann wird der gemerkte Wert an den Argumentnamen gebunden. Wenn der Wert ein veränderbares Objekt ist und man es verändert, dann ist diese Änderung natürlich auch bei folgenden Funktionsaufrufen sichtbar.

Die Argumente und die Defaultwerte, die sich eine Funktion merkt, kann man sich auch anschauen. Das letzte Element im Ergebnis von `inspect.getargspec()` ist ein Tupel mit den Defaultwerten:

Code: Alles auswählen

In [388]: def f(a, b=list()):
   .....:     b.append(a)
   .....:     return b
   .....:

In [389]: inspect.getargspec(f)
Out[389]: (['a', 'b'], None, None, ([],))

In [390]: f(42)
Out[390]: [42]

In [391]: inspect.getargspec(f)
Out[391]: (['a', 'b'], None, None, ([42],))

In [392]: x = f(42)

In [393]: inspect.getargspec(f)
Out[393]: (['a', 'b'], None, None, ([42, 42],))

In [394]: x.append(23)

In [395]: inspect.getargspec(f)
Out[395]: (['a', 'b'], None, None, ([42, 42, 23],))
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Sonntag 29. August 2010, 08:53

Danke, das war sehr aufschlussreich. Für mich heißt das: keine veränderbaren Defaultwerte benutzen, sondern dann den vorgeschlagenen Weg über `None` und Auswertung `if not a: a = []` gehen.

Dieses Verhalten ist aber nur bei veränderbaren Typen so, richtig? Also nicht bei Strings, Zahlen, Bool und Tuple? Die sind ja auch noch dasselbe Objekt nach dem Initialisieren und lassen sich im Laufe der Funktion verändern...
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

Sonntag 29. August 2010, 09:06

droptix hat geschrieben:Danke, das war sehr aufschlussreich. Für mich heißt das: keine veränderbaren Defaultwerte benutzen, sondern dann den vorgeschlagenen Weg über `None` und Auswertung `if not a: a = []` gehen.
Nein, du solltest schon `if a is None: a = []` verwenden. Bei deiner Variante erwischt du auch an die Funktion übergebene leere Listen.
Dieses Verhalten ist aber nur bei veränderbaren Typen so, richtig? Also nicht bei Strings, Zahlen, Bool und Tuple? Die sind ja auch noch dasselbe Objekt nach dem Initialisieren und lassen sich im Laufe der Funktion verändern...
Das Verhalten ist immer gleich, bloß dass es dir bei unveränderlichen Objekten meist egal ist.
Pekh
User
Beiträge: 482
Registriert: Donnerstag 22. Mai 2008, 09:09

Sonntag 29. August 2010, 09:09

Ich habe irgendwann mal gelernt, daß es schlechter Stil sei, übergebene Parameter überhaupt zu verändern. Wenn du die Daten unbedingt verändern mußt, solltests du auf Kopien arbeiten oder solche nach und nach anlegen. Grund sind die ja schon beobachteten Nebeneffekte.
BlackJack

Sonntag 29. August 2010, 09:35

@Pekh: Ich denke man muss da zwischen "Funktionen" und "Prozeduren" unterscheiden. Bei "Funktionen" sollte man möglichst nichts verändern, also keine Seiteneffekte produzieren. Es gibt ja aber auch Funktionen, deren ausgewiesener Zweck es ist, übergebene Objekte zu verändern. Beispiel wäre die Funktion zum "Verbinden" von Dreiecken, die sich eine gemeinsame Linie teilen, die ich in einem anderen Thread gepostet hatte:

Code: Alles auswählen

def connect_triangles(triangles):
    for triangle_a in triangles:
        for triangle_b in triangles:
            if (triangle_a is not triangle_b
                    and triangle_a.has_shared_line_with(triangle_b)):
                triangle_a.add_neighbour(triangle_b)
                triangle_b.add_neighbour(triangle_a)
Man könnte hier natürlich vorher eine Kopie von den Dreiecken machen und danach eine Liste mit den Kopien zurückgeben, aber die Fragen bei so etwas sind dann oft:

1) Brauche ich überhaupt eine Kopie oder wird in 99% der Anwendungsfälle die alte Variante gleich nach dem Aufruf der Funktion sowieso verworfen?

2) Wie tief darf/muss man überhaupt kopieren? So wirklich problemlos geht das nur bei Wertobjekten, also solchen, die keinen veränderbaren Zustand haben. Veränderbarer Zustand ist aber im Allgemeinen bei Objekten üblich.

Die zweite Frage erfordert mehr Wissen über die inneren Details von Dreiecksobjekten als man in dieser Funktion vielleicht haben möchte.
Antworten