Seite 1 von 1

Verstehe ich nicht: lokale Variable wie global

Verfasst: Donnerstag 26. August 2010, 21:58
von droptix
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.

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Donnerstag 26. August 2010, 22:03
von droptix
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.

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Donnerstag 26. August 2010, 22:03
von cofi
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".

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Samstag 28. August 2010, 16:06
von droptix
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?

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Samstag 28. August 2010, 16:14
von DaMutz
es ist egal:

Code: Alles auswählen

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

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Samstag 28. August 2010, 17:10
von BlackJack
@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],))

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Sonntag 29. August 2010, 08:53
von droptix
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...

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Sonntag 29. August 2010, 09:06
von Darii
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.

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Sonntag 29. August 2010, 09:09
von Pekh
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.

Re: Verstehe ich nicht: lokale Variable wie global

Verfasst: Sonntag 29. August 2010, 09:35
von BlackJack
@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.