Einfache rekursive Funktion komisches Verhalten

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
Denn1s
User
Beiträge: 17
Registriert: Mittwoch 8. Januar 2014, 14:08

Hallo,
ich habe folgenden code:

Code: Alles auswählen

class object_list():
    def __init__(self):
        self.sub_objects = []
    def get_objects(self, ret=[]):
        ret.extend(self.sub_objects)
        for ob in self.sub_objects:
            ret = ob.get_objects(ret)
        return ret
Im Grunde ist ein ganz einfache Struktur. Jedes Objekt darf Unterobjete enthalten. Mit get_objects möchte ich nun alle Unterobjekte in einer Liste abspeichern.
Allerdings liefert:

Code: Alles auswählen

A = object_list()
B = object_list()
C = object_list()
A.sub_objects.append(B)
print(A.get_objects()) # liste mit einem objekt
print(A.get_objects()) # liste mit zwei objekten
print(A.get_objects()) # liste mit drei objekten
...
Kann mir jemand das Verhalten erklären. Klar ich könnte einfach A.get_objects(ret=[]) . Mein debugger zeigt, dass beim mehrmaligen Aufruf der Funktion der ret Wert nicht verschwindet. Ich verstehe im Grunde das Verhalten von Python nicht. Klar kann man das Ganze auch eleganter schreiben : )

Danke für jeden Hinweis!

Gruß Dennis

edit: ich nutze python3.3
Zuletzt geändert von Denn1s am Freitag 14. Februar 2014, 16:52, insgesamt 1-mal geändert.
BlackJack

@Denn1s: Default-Werte bei Funktionen und Methoden werden genau *einmal* ausgewertet, nämlich dann wenn die Funktion oder Methode ``def``\iniert wird, und nicht bei jedem Aufruf. Es gibt also nur *eine* Liste die immer wieder an `ret` gebunden wird und die Du veränderst/erweiterst. Statt ``ret=[]`` müsstest Du also ``ret=None`` schreiben und in der Methode testen ob es `None` ist und falls ja, eine *neue* leere Liste an den Namen binden.

Edit: Dann würde die Methode übrigens so immer noch recht eigenartig aussehen. Denn sie verändert etwas *und* gibt etwas zurück. Mindestens mal das ``ret =`` in der Schleife würde ich weg lassen, das ist verwirrend weil es den Umstand verschleiert das dieses `ret` immer die *selbe* Liste ist.

Edit2: Und dann ist der Name `get_objects()` etwas komisch. Ich würde das dann in zwei Methoden trennen, eine `get_objects()` heisst und eine Liste mit allen Objekten liefert und eine interne die diese Liste rekursiv erstellt. Dann braucht man auch keinen Default-Wert, denn die leere Liste kann auf oberster Ebene von der `get_objects()` übergeben werden.
Denn1s
User
Beiträge: 17
Registriert: Mittwoch 8. Januar 2014, 14:08

Ok ehrlichgesagt verstehe ich das Verhalten trotzdem nicht. Gibt es dazu ein Tutorial oder eine Erklärung in einem Buch?
Soweit ich es Verstanden habe beinhaltet meine Funktion als "instanz variable" die Liste ret. Aber wäre nicht das natürliche Verhalten, dass diese Liste nach jedem Aufruf gelöscht wird?
Besteht dadurch nicht allgemein Gefahr, dass Werte übergeben werden die man nicht übergeben möchte?
z.B. def kord(self, x = 0, y=0) -> würde es dann heißen, dass falls ich einmal diese Funktion aufrufe kord(x=2,y=3) der nächste Aufruf kord() als kord(x=2,y=3) interpretiert wird?

edit: Ich weiß der Code ist nicht wirklich schön :) (ist aber auch nicht von mir). Es ging mir nur ums Verständnis!
Sirius3
User
Beiträge: 18216
Registriert: Sonntag 21. Oktober 2012, 17:20

@Denn1s: Beim Definieren einer Funktion werden ihre Defaultwerte gespeichert, in Deinem Fall ist der Defaultwert eine Liste. Wenn Du nun den Inhalt dieser Liste änderst, dann ändert sich nicht der Defaultwert, sondern der Inhalt des Defaultwertes. Bei x=0 kann Dir das nicht passieren, weil 0 unveränderlich ist.

Statt dass die Funktion eine Liste bekommt und diese erweitert, sollte der Aufrufer die Listenelemente sammeln:

Code: Alles auswählen

class object_list():
    def __init__(self):
        self.sub_objects = []
    def get_objects(self):
        ret = list(self.sub_objects)
        for ob in self.sub_objects:
            ret.extend(ob.get_objects())
        return ret
BlackJack

@Denn1s: `ret` ist keine Instanzvariable, das ist ein ganz normaler lokaler Name. Das Verhalten ist auch bei Funktionen so. Wenn die ``def``-Anweisung ausgeführt wird, wird *einmalig* ``[]`` ausgeführt, also eine leere Liste erstellt, und im Methoden-Objekt gespeichert. Und jedes mal wenn Du die Methode ohne Argument für `ret` aufrufst wird diese Liste an den Namen `ret` gebunden. Immer diese eine, selbe Liste. Es wird nicht bei jedem Aufruf eine neue erstellt. Und wenn Du die Liste innerhalb der Methode *veränderst*, dann ist diese Veränderung natürlich auch beim nächsten Aufruf sichtbar, denn es handelt sich ja um die selbe Liste.

Bei nicht veränderbaren Objekten wie Zahlen kann das nicht passieren, weil man die ja, nun ja, eben nicht verändern kann, also kann beim nächsten Aufruf auch keine Veränderung sichtbar sein.

Also noch mal: Wenn die ``def``-Anweisung für die Methode ausgeführt wird, wird *einmalig* der Default-Wert ausgewertet. Also eine leere Liste.

Wenn `A.get_objects()` das erste mal aufgerufen wird, dann wird diese Liste an den lokalen Namen `ret` gebunden und in der Methode fügst Du ein Objekt hinzu.

Wenn `A.get_objects()` wieder aufgerufen wird, dann wird die Default-Liste in der sich schon ein Objekt befindet an den lokalen Namen `ret` gebunden und Du fügst wieder ein Objekt hinzu. Und so weiter. Der Ausdruck für den Default-Wert wird *nicht* beim Aufruf ausgewertet.
Benutzeravatar
snafu
User
Beiträge: 6831
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Denn1s hat geschrieben:Besteht dadurch nicht allgemein Gefahr, dass Werte übergeben werden die man nicht übergeben möchte?
z.B. def kord(self, x = 0, y=0) -> würde es dann heißen, dass falls ich einmal diese Funktion aufrufe kord(x=2,y=3) der nächste Aufruf kord() als kord(x=2,y=3) interpretiert wird?
Zahlen werden üblicherweise durch neue Zahlobjekte ersetzt, wenn mit einem anderen Wert gearbeitet werden soll. Das heißt: ``1 + 1`` ergibt das neue Zahlobjekt ``2``. Es ist nicht so, dass die linke Zahl sozusagen um eins ansteigt und dabei das selbe Objekt bliebe. Merk dir einfach, dass veränderbare Objekte (und das sind z.B. Listen, Wörterbücher und Mengen) in Python definitiv nicht als Defaultwert in Methoden oder Funktionen genutzt werden sollten.
BlackJack

@snafu: Ausser man möchte dieses Verhalten haben, zum Beispiel wenn man einen Cache für Rückgabewerte implementieren möchte.
Benutzeravatar
snafu
User
Beiträge: 6831
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

BlackJack hat geschrieben:@snafu: Ausser man möchte dieses Verhalten haben, zum Beispiel wenn man einen Cache für Rückgabewerte implementieren möchte.
Oder man nutzt symbolisch einen leeren Container, um den erwarteten Datentyp zu dokumentieren. Dabei müsste man dann aber natürlich beachten, dass dieser leere Container nicht innerhalb der Funktion befüllt werden darf. Stattdessen müsste in der Funktion ein neuer Container erstellt werden, falls kein befüllter Container vom Aufrufer übergeben wurde. Das "definitiv" in meiner letzten Aussage war also definitiv zu voreilig. ;)
Denn1s
User
Beiträge: 17
Registriert: Mittwoch 8. Januar 2014, 14:08

Bei Konstruktoren besteht die Gefahr aber nicht , ist das Richtig? Also ich darf ohne Bedenken def __init__(self,list_1=[],list_2=[],etc.) einsetzen.
Sirius3
User
Beiträge: 18216
Registriert: Sonntag 21. Oktober 2012, 17:20

@Denn1s: Erstens ist "__init__" kein Konstruktor und zweitens eine ganz normale Methode, bei der das selbe gilt, wie für andere Funktionen auch.
BlackJack

@Denn1s: Warum sollte das bei der `__init__()` anders sein? Das ist generell bei Funktionen und Methoden so.
Benutzeravatar
snafu
User
Beiträge: 6831
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Denn1s hat geschrieben:Bei Konstruktoren besteht die Gefahr aber nicht , ist das Richtig? Also ich darf ohne Bedenken def __init__(self,list_1=[],list_2=[],etc.) einsetzen.
Darfst du wie gesagt so oder so machen. Du solltest dann nur sowas in der Art schreiben:

Code: Alles auswählen

def __init__(self, data=[]):
    self.data = data if data else []
Damit wäre sichergestellt, dass die vorgegebene leere Liste nicht angefasst wird.

Alternativ zu ``self.data = data if data else []`` geht auch das kürzere ``self.data = data or []``.
Denn1s
User
Beiträge: 17
Registriert: Mittwoch 8. Januar 2014, 14:08

Habs gerade ausprobiert:

Code: Alles auswählen

class object_list():
    def __init__(self, ids=[]):
        self.ids = ids
        self.sub_objects = []
    def get_objects(self, ret=[]):
        ret.extend(self.sub_objects)
        for ob in self.sub_objects:
            ret = ob.get_objects(ret)
        return ret
A = object_list(ids=[12])
B = object_list()
print(A.ids) #[12]
print(B.ids) #[]
Sirius3
User
Beiträge: 18216
Registriert: Sonntag 21. Oktober 2012, 17:20

@Denn1s: Du solltest das ausprobieren:

Code: Alles auswählen

A = object_list()
B = object_list()
A.ids.append(12)
print(B.ids)
Denn1s
User
Beiträge: 17
Registriert: Mittwoch 8. Januar 2014, 14:08

Ja :) kommt 12 raus. Da muss man ja echt höllisch aufpassen... warum kriege ich aber beim Konstruktor vorhin nicht 12 raus?
BlackJack

@Denn1s: Weil Du bei `A` eine Liste mit einer 12 drin übergeben hast und damit *diese* Liste verwendet wurde und bei `B` hast Du nichts übergeben womit dort die Default-Liste verwendet wurde. Das sind zwei verschiedene Listen.

Vielleicht hilft ja dieses sinnfreie Minimalbeispiel:

Code: Alles auswählen

In [5]: def f(parrot, spam=[]):
   ...:     spam.append(parrot)
   ...:     return spam
   ...: 

In [6]: f.func_defaults
Out[6]: ([],)

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

In [8]: f.func_defaults
Out[8]: ([42],)

In [9]: f(23)
Out[9]: [42, 23]

In [10]: f.func_defaults
Out[10]: ([42, 23],)

In [11]: f(4711, [])
Out[11]: [4711]

In [12]: f.func_defaults
Out[12]: ([42, 23],)

In [13]: f.func_defaults[0][0] = 'hallo'

In [14]: f('welt')
Out[14]: ['hallo', 23, 'welt']
Bei der Ausführung des ``def`` bei [5] wird eine Liste erstellt und im Funktionsobjekt gespeichert. Kann man bei [6] sehen (bin mir fast sicher dass der Name `func_defaults` ein Implementierungsdetail ist, also besser keinen Code schreiben der sich darauf verlässt).

Immer wenn man jetzt `f()` ohne ein Argument für `spam` aufruft wird diese Liste an den lokalen Namen `spam` gebunden. Ansonsten halt der Wert den man übergeben hat.

Wenn man das Objekt in der Methode verändert, nun ja, dann verändert man natürlich das Objekt. Wie man sowohl am Effekt bei den Aufrufen, als auch beim nachsehen in den Funktionsinterna deutlich sehen kann. Und wenn man direkt in den Innereien herum pfuscht, sieht man das natürlich auch bei den folgenden Aufrufen.
Denn1s
User
Beiträge: 17
Registriert: Mittwoch 8. Januar 2014, 14:08

Besten Dank, hat sehr weitergeholfen!!! Ich glaube nun muss ich noch einiges in meinem Code korrigieren :)
Schönes WE Euch allen!
BlackJack

Offizielle API um an die Werte zu kommen:

Code: Alles auswählen

In [20]: import inspect

In [21]: inspect.getargspec(f)
Out[21]: ArgSpec(args=['parrot', 'spam'], varargs=None, keywords=None, defaults=(['hallo', 23, 'welt'],))
Antworten