Listen als Default-Parameter (Memory Leak)

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
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Guten Morgen Forum,

ich haben in einem Programm gerade ein Speicherproblem. Das konnte ich recht schnell eingrenzen, weil dort in Funktionen Listen als Default-Parameter benutzt werden.

Ich habe gelesen, dass das böse ist, aber noch nicht so ganz verstanden warum.

Ich habe eine Klasse "Article" in der es folgende Funktion gibt:

Code: Alles auswählen

    def update_materials(self, checked_articles=[]):
        checked_articles.append(self)
        [...]
        for article in self.child_articles:
            if article not in checked_articles:
                article.update_materials(checked_articles)
Also änderte ich die Funktion auf:

Code: Alles auswählen

    def update_materials(self, checked_articles=None):
        if not checked_articles:
            checked_articles = []
        checked_articles.append(self)
        [...]
        for article in self.child_articles:
            if article not in checked_articles:
                article.update_materials(checked_articles)
Reicht das? Oder mache ich hier einen blöden Gedankenfehler?
Der Interpreter zieht nach wie vor Speicher und gibt ihn nicht wieder frei, aber dann weiß ich, dass ich woanders suchen muss ;)
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Listen als Default-Argument zu benutzen ist "boese", weil immer diesselbe Liste benutzt wird. Das kann durchaus in einem Memory-Leak enden, weil diese Liste nie freigegeben wird.

Mit dem `None` Ansatz - gibt es eigtl schon einen Namen fuer das Pattern? - behebst du das theoretische Problem.

Die Tools hier koennten dir weiterhelfen:

https://stackoverflow.com/questions/552 ... -in-python
https://stackoverflow.com/questions/110 ... ecommended
BlackJack

@sparrow: Dieses Listenobjekt wird genau *einmal* erstellt, nämlich wenn die ``def``-Anweisung ausgeführt wird. Und bei jedem *Aufruf* der Methode wird das *selbe* Listenobjekt verwendet wenn dafür kein Argument übergeben wird. Änderungen an der Liste sind also beim nächsten Aufruf sichtbar, weil es ja nur dieses eine Objekt gibt.

Das Idiom mit `None` verhindert das, weil dann jedes mal wenn kein Argument übergeben wurde, eine *neue* Liste erstellt wird.
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Würde das Problem auffallen, wenn ich mir die Länge der Liste anzeigen lasse?
Also wenn immer nur die selbe Liste verwendet wird, wächst die dann mit jedem append?
BlackJack

@sparrow: Natürlich wächst die Anzahl der Elemente mit einem `append()`. Falls nicht, wäre `append()` irgendwie kaputt, oder. ;-)

Edit: Mal so am Rande ist eine Menge (`set`) für `checked_articles` vielleicht die semantisch bessere Wahl. Zumindest solange sich `self` passend verhält was die Werte und `hash()`/``==`` angeht.
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Irgendwie ist das einzige, das gerade kaputt ist, mein Kopf ;)

Bei den Listen als Default-Parametern dachte ich, dass es das Problem lösen würde... war aber leider nicht so. Die scheinen, falls sie vorher kaputt waren als Default-Parameter, jetzt auf jeden Fall richtig zu funktionieren.

Gibt es noch eine tückische Falle? Irgend etwas, das nicht abgeräumt wird, wenn eine Funktion verlassen wird?

Ich würde ja gerne den kompletten Quellcode zeigen, aber das ist wirklich viel.
BlackJack

@sparrow: Hast Du vielleicht irgendwo `__del__()` implementiert oder verwendest etwas was diese Methode implementiert? Externe Bibliotheken die irgendwo im C-Teil ein Leck haben?

Und wie äussert sich das überhaupt? Wird wirklich immer *mehr* Speicher verwendet im Laufe der Zeit, oder einfach nur sehr viel aber dann eher konstant und Du erwartest, das der Speicher auch irgendwann dem Betriebssystem zurückgegeben wird?

Edit: Etwas weiter oben hat jemand auf StackOverflow zum Thema Python/Memory profiling verlinkt — hast Du schon mal geschaut wo der Speicherverbrauch liegt?
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

BlackJack hat geschrieben:@sparrow: Hast Du vielleicht irgendwo `__del__()` implementiert oder verwendest etwas was diese Methode implementiert? Externe Bibliotheken die irgendwo im C-Teil ein Leck haben?
Nein, kein __del__() und an Bibliotheken wird nur das Django-ORM verwendet.
BlackJack hat geschrieben:Und wie äussert sich das überhaupt? Wird wirklich immer *mehr* Speicher verwendet im Laufe der Zeit, oder einfach nur sehr viel aber dann eher konstant und Du erwartest, das der Speicher auch irgendwann dem Betriebssystem zurückgegeben wird?
Ich habe in Django ein Model, dass die o.g. Funktion enthält. Zum Testen dient im Augenblick:

Code: Alles auswählen

def update(article):
    print "Updating article:", article.id
    article.update_materials()
    print "done!"

for article in Article.objects.all():
    update(article)
Ich gehe davon aus, dass spätestens mit dem Verlassen von "update" der Speicher darin zum Abräumen bereit steht und sich der GC irgendwann darum kümmert. Es ist aber so, dass der Speicherbedarf des Programms während der Ausführung immer mehr zunimmt, bei irgend etwas jenseits der GB-Grenze kommt es dann zu einem Fehler und es wird abgebrochen (MemoryException? Es meint Speicher voll, ob es genauso heißt weiß ich nicht).
Der Speicher wird erst wieder freigegeben, wenn ich den Interpreter dann mit quit() beende.

Der Speicher läuft auch nicht innerhalb eines Durchlaufs von update voll. Die Print-Anweisungen sollen zeigen, dass die Bearbeitung eines Artikels abgeschlossen ist, und die kommen regelmäßig. Aber irgend etwas bleibt im Speicher liegen, und das mit jedem Aufruf wieder.
BlackJack

@sparrow: Direkt in `update()` selber gibt es ja keinen Speicher der belegt wird denn `article` kommt als Argument rein, hat also ausserhalb der Funktion schon eine Referenz. Wird denn irgend etwas an die `article`-Objekte gebunden? Läuft die Schleife ausserhalb bis zum Ende durch? Falls nicht, wie kommt die? Wieviele `article` von wievielen insgesamt werden verarbeitet?
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

BlackJack hat geschrieben:@sparrow: Direkt in `update()` selber gibt es ja keinen Speicher der belegt wird denn `article` kommt als Argument rein, hat also ausserhalb der Funktion schon eine Referenz.
Genau aus dem Grund habe ich das in diese eher unsinnige Funktion ausgelagert. Also zum Testen.
BlackJack hat geschrieben:Wird denn irgend etwas an die `article`-Objekte gebunden?
An welcher Stelle müsste es denn an "article" gebunden werden, um sowohl nach dem Rücksprung aus "update()" als auch in dem nächsten Durchlauf der for-Schleife noch irgendwo im Speicher zu liegen?
Der Aufruf erfolgt genau wie oben beschrieben. Nichts davor, nichts dahinter. Einfach mit "manage.py shell" eine Django-Shell geöffnet, "Article" importiert und dann die entsprechenden Eingaben wie oben.
BlackJack hat geschrieben:Läuft die Schleife ausserhalb bis zum Ende durch? Falls nicht, wie kommt die? Wieviele `article` von wievielen insgesamt werden verarbeitet?
Die for-Schleife? Sie schafft, je nachdem wie viel Speicher frei ist, zwischen 1.000 - 2.000 Artikeln, von mehr als 20.000.
BlackJack

@sparrow: An welcher Stelle etwas an die Objekte gebunden wird ist egal, die Objekte selbst und damit natürlich auch alles was daran gebunden wird, bleiben ja im Speicher solange das Objekt irgendwie von Code erreichbar sind. Und wenn man jetzt einfach mal davon ausgeht das die `all()`-Methode die in einem Container wie einer Liste oder einem Wörterbuch hält, dann werden die `Article`-Exemplare und alles was da dran hängt, frühenstens nach dem Abarbeiten der kompletten Schleife freigegeben. Falls das ORM die nicht sogar in einem Cache noch länger aufhebt. Und wenn man bei den ORM-Objekten irgendwas verwendet was aus der DB geholt wird, also auch bei scheinbar harmlos aussehenden Attributzugriffen, entstehen ja auch Objekte im Speicher die an den ORM-Objekten hängen und vielleicht auch wieder gecachet werden. Da ist die Frage wieviel vom Datenbankinhalt während dieser Verarbeitung in den Speicher gezogen wird.

Schau Dir doch mal den zusätzlichen Speicherverbrauch pro verarbeitetem Artikel an. Da kann man dann schauen ob das halbwegs gleichmässig verteilt ist, oder ob es Aussreisser gibt die man sich mal genauer anschauen sollte.
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Manchmal müssen diese Materialien neu berechnet werden. Im Laufenden Betrieb ist halt doof, dass sich dadurch die laufenden WSGI-Prozesse immer weiter aufblähen, bis die Kiste platzt. Der Speicherverbrauch stapelt sich halt und wird in den wenigsten Fällen für alle Artikel ausgeführt.

Ich werde jetzt erst einmal schauen, ob ich diesen Aufruf nicht in einen eigenen Prozess kapsele. Wenn der dann abgeschlossen ist, weiß ich auf jeden Fall, dass der Speicher wieder freigegeben wird.

Ansonsten bin ich gerade ratlos. Ich dachte mit den Default-Parametern hätte ich das Problem gefunden, weil es sich ebenso verhält. Ich sehe zumindest von meiner Seite aus keine Referenz, die dem erneuten Durchlauf der for-Schleife noch da sein dürfte. Und es muss irgendwas mit den Funktionen in Article zu tun haben, weil das Problem halt auch dann auftritt, wenn ich willkürlich Artikel nehmen und immer wieder verwende.

Also:

Code: Alles auswählen

a = Article.objects.get(id=1)
while True:
    a.update_materials()
Nur damit ich das richtig verstehe, vielleicht bin ich auch nach all den Jahren noch zu Java-lastig, wenn ich da in update_materials() Listen baue und Dinge tue, und ich die nicht von außen her referenziere, dann werden die schon abgeräumt wenn a nicht mehr gültig ist, richtig?
Benutzeravatar
sparrow
User
Beiträge: 4193
Registriert: Freitag 17. April 2009, 10:28

Sorry, Doppelpost.

Hier das kleinste Beispiel, das bei mir nicht klappt:

ich habe eine Datei test.py:

Code: Alles auswählen

from webapp.models import Article

def main():
    while True:
        a = Article.objects.get(id=1)
        a.update_materials()
in Article:

Code: Alles auswählen

    def update_materials(self, checked_charges=None):
        if not checked_charges:
            checked_charges = set([])
Die hört da wirklich auf. Das war es. Zum Testen habe ich nicht mehr drinnen gelassen.

In der Shell:

Code: Alles auswählen

python manage.py shell
>>> import test
>>> test.main()
Und trotzdem läuft der Speicher voll?


Edit:
Och nööööööööö. Das ist ja noch viel schlimmer.
Eigentlich reicht es schon immer wieder das gleiche Objekt aus der Datenbank zu laden :(

Code: Alles auswählen

def main():
    while True:
        a = Article.objects.get(id=1)
Das reicht schon völlig aus.


Edit 2:

Hah, gefunden.

Code: Alles auswählen

from django import db
# und dann am Ende der gewünschten Funktion
db.reset_queries()
Eigentlich soll Django das nach dem Ende eines jeden Requests machen (gut, das waren jetzt keine Requests, wir haben ja über die Shell zugegriffen), aber ich habe das Problem halt auch im laufenden Betrieb gehabt.
Antworten