'Funktionszeiger' und Garbage-Collection

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
loph
User
Beiträge: 4
Registriert: Freitag 3. November 2023, 22:32

Hallo Forum,

auch wenn ich weiß, dass es Funktionszeiger an sich in Python nicht gibt, ist mir kein besserer Titel eingefallen.

Folgendes Problem (Code-Beispiel im Ende des Postings):
Ich habe eine Klasse, in der eine Member-Variable auf eine Member-Method zeigt (und entsprechend auch auf andere Funktionen zeigen kann). Nun möchte ich eine Instanz dieses Objekts sauber löschen (bzw. entfernen). Dazu verwende ich 'del <Klassen-Objekt>'.

Ich denke der Speicher wird erst dann freigegeben (bzw. 'del' wird erst ausgeführt), wenn der interne Instanzen-Zähler meines Klassenobjects auf 0 heruntergezählt ist. Daher verhindert meine Membervariable auf die Member-Method ein Freigeben des Objekts. Deshalb habe ich am Ende meines Testprogramms eine ganze Reihe von del-Anweisungen, bei denen die verwaisten Objekte freigegeben werden.

Rufe ich nach 'del <Klassenobjekt>' per Hand den Garbage-Collecter 'gc.collect()' auf, werden die Instanzen schon zur Programm-Laufzeit freigegeben, was letzten Endes mein gewünschtes Verhalten wäre.

Gibt es einen Weg dieses Verhalten in sauberen Code in meine Klasse zu implementieren?

Test-Programm (kommentiert man die Zeile 'gc.collect()' aus, so werden die Objekte erst nach Programm-Ende vom Garbagecollector freigegeben):

Code: Alles auswählen

import gc


class a:
    def __init__(self, a=0):
        self.a = a
        self._fkt = self._fkt_a

    def print_fkt(self):
        self._fkt()

    def set_fkt(self, sel: str):
        if sel == 'a':
            self._fkt = self._fkt_a
        elif sel == 'b':
            self._fkt= self._fkt_b

    def _fkt_a(self):
        print("A")

    def _fkt_b(self):
        print("B")

    def __del__(self):
        print(f"__del__: self.a={self.a}")
        #no super().__del__()?
        #super().__del__()


if __name__ == '__main__':
    o = a(99)
    o.print_fkt()
    for i in range(10):
        del o
        
        gc.collect()
        # Question:
        # Is there a clean way to get o deleted 'properly',
        # withougt the use of gc?
        o = a(i)

    o.set_fkt('b')
    o.print_fkt()

Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Vergiss dass es del gibt und vergiss auch, dass man händisch den garbage collector aufrufen kann.
Python kümmert sich wunderbar selbst um die Speicherverwaltung.
Warum meinst du, dass du dich selbst darum kümmern musst?
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich schreibe seit etwa 25 Jahren Python. __del__ habe ich dabei nie benutzen müssen. Gelegentlich bei tatsächlichen Speicherlecks Zyklen aufbrechen kam vor, aber auch alle Schaltjahre mal. Da ist ein nicht-Problem in der echten Welt.

Wen du kontrolliert Ressourcen freigeben willst, kann das Kontextmanager-Muster helfen.
Benutzeravatar
sparrow
User
Beiträge: 4195
Registriert: Freitag 17. April 2009, 10:28

@loph und last but not least: Programme gehören in Funktionen. Das ist fundamental, damit der Lebensbereich von Objekten entsprechend eingeschränkt ist.
Dein gewünschtes Verhalten deutet, genau wie die schlechte Namensgebung, auf irgend eine andere Sprache hin - aber sicher nicht auf Python.
loph
User
Beiträge: 4
Registriert: Freitag 3. November 2023, 22:32

Vielen Dank für die zügigen Antworten! :)

Natürlich geht es mir bei meiner Frage auch darum ein besseres Verständnis von Python zu erlangen.

Trotzdem hat das Problem einen handfesten Hintergrund:

Das Programm, das ich schreibe soll plattformunabhängig (Mac, Win, Linux) überaus stabil (viele Stunden Laufzeit) Midi-Daten zwischen Computer und Hardware-Synthesizer verteilen.
Dazu verwende ich eine Midi-Bibliothek (rtMidi), die offenkundig bei der ALSA-Implementierung (Linux) auf einen Bug stößt. Das äußert sich so, dass man nicht mehr als 64 midi-Objekte gleichzeitig anfordern kann.
Für dieses spezifische Problem sind mir zwar schon ein paar Workarounds eingefallen (schließlich scheint die Lösung mit gc.collect() das Problem ja tatsächlich zu beheben), aber um zukünftige ähnlich Probleme auszuschließen, hätte ich gerne die 'sauberste' Lösung.

Außerdem lerne ich was dabei :)

Grüße
loph
nezzcarth
User
Beiträge: 1635
Registriert: Samstag 16. April 2011, 12:47

Hattest du mal geschaut, ob du https://docs.python.org/3/library/weakref.html ggf. ein Baustein für eine Lösung sein kann?
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wenn es Bugs gibt, spricht ja nichts gegen explizites cleanup bzw aufbrechen von Referenzen. Aber das in __del__ zu haengen halte ich trotzdem fuer keine gute Idee. Was besser funktioniert, muesste man am konkreten Code besprechen.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Dann musst du in deinem Programm die Midi-Ressourcen explizit freigeben. Kontextmanager wurden ja schon angesprochen. Garbage collector ist nicht die saubere Lösung. Ohne das Programm zu kennen ist es aber auch schwierig deinen Fehler zu finden.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@loph: Vielleicht noch mal etwas konkreter warum `__del__()` keine Lösung ist: Das ist kein deterministischer Destruktor wie beispielsweise C++ den hat. Selbst mit ``del``, manuellem aufrufen von `gc.collect()`, und was auch immer man sich sonst noch einfallen lässt, garantiert die Sprachdefinition *explizit* nicht nur nicht *wann* diese Methode aufgerufen wird, sondern nicht einmal ob sie *überhaupt* aufgerufen wird. Schlimmer noch: das vorhandensein von einer `__del__()`-Methode kann sogar dazu führen das Objekte nicht freigegeben werden können, weil die Speicherbereinigung sich dann nicht ”traut” Kreise im Objektgraph aufzulösen. Wenn man also etwas hat, was wirklich zwingend aufgeräumt werden muss, kann man `__del__()` nicht verwenden. Und dann ist das in der Praxis genau so ”wertlos” wie beispielsweise `finalize()` in Java. Deswegen die Aussage von langjährigen Python-Entwicklern, dass das in der Praxis so gut wie nie vorkommt das man eine `__del__()`-Methode schreibt.

Ich habe gerade mal einen Blick in die Doku von `python-rtmidi` geworfen: Das die in Beispielen ``del`` benutzen, auf einem Midi-Objekt das in dem Beispiel vorher als Kontextmanager mit ``with`` verwendet wurde riecht komisch. Also ``del`` alleine wäre schon schräg, weil das *Namen* löscht, und keine *Objekte*. Die API die das Projekt bietet ist an der Stelle schräg. Entweder räumen die Midi-Objekte als Kontextmanager nicht ausreichend hinter sich auf, oder die Programmierer haben in dem Midi-Objekt zu viel drin und es sollte eigentlich ein Midi-Objekt geben als Kontextmanager das `Port`-Objekte liefert, die ihrerseits Kontextmanager sind, und die jeweils ihren ”Bereich“ aufräumen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@loph: Vielleicht trotzdem noch mal was zum Quelltext aus dem ersten Beitrag. Ich weis Namen in Beispielen, aber auch in Beispielen ist es unnötig verwirrend wenn man sich nicht an Konventionen hält und/oder im gleichen Namensraum `a` einmal die Klasse und kurze Zeit später ein Exemplar benennt, und an anderer Stelle für eine Zahl steht.

Selbst wenn man einen Wert (potentiell) an einer Stelle im Programmfluss für die Speicherbereinigung freigeben wollen würde, würde man da kein ``del`` für verwenden. Denn ``del`` löscht wie gesagt *Namen*. Gerade wenn man dann kurz danach den Namen wieder an einen Wert bindet, dann will man ja ganz offensichtlich gar nicht den Namen löschen. Das ein Name oder Attribut nicht mehr an einem Objekt festhält was man gerne zum abräumen freigeben möchte drückt man üblicherweise durch ``instance = None`` aus.

Das Beispiel würde man in Python so schreiben können (ungetestet):

Code: Alles auswählen

class Thing:
    def __init__(self, value=0):
        self.value = value
        self.print = self._print_a

    def set_action(self, what):
        self.print = getattr(self, f"_print_{what}")

    def _print_a(self):
        print("A")

    def _print_b(self):
        print("B")

    def close(self):
        print(f"close: {self.value=}")


def main():
    thing = Thing(99)
    try:
        thing.print()
        for i in range(10):
            thing.close()
            thing = Thing(i)

        thing.set_action("b")
        thing.print()
    finally:
        thing.close()


if __name__ == "__main__":
    main()
Da hier ein `Thing` durch ein anderes ersetzt werden muss, würde man das besser in einen weiteren Datentyp (oder vielleicht etwas exotischer in eine Generatorfunktion) kapseln wo diese Regel durchgesetzt wird, falls das wirklich immer so sein *muss*. Falls das nicht immer so sein *muss*, würde es sich eventuell lohnen aus dem `Thing` einen Kontextmanager zu machen (``with``, `__enter__()`, `__exit__()`).
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
loph
User
Beiträge: 4
Registriert: Freitag 3. November 2023, 22:32

Hallo Forum,

nachdem mich Komplikationen einer Zahn-OP eine Woche von den Beinen geholt haben, möchte ich jetzt trotzdem mit etwas Verspätung für die vielen hilfreichen Antworten bedanken!

In Zukunft werde ich mich auch bemühen bessere Codebeispiele zu posten! :)
Antworten