16 Stimmen

Python: yield-and-delete

Wie kann ich ein Objekt aus einem Generator erzeugen und sofort vergessen, sodass es keinen Speicher belegt?

Zum Beispiel in der folgenden Funktion:

def grouper(iterable, chunksize):
    """
    Return elements from the iterable in `chunksize`-ed lists. The last returned
    element may be smaller (if length of collection is not divisible by `chunksize`).

    >>> print list(grouper(xrange(10), 3))
    [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
    """
    i = iter(iterable)
    while True:
        chunk = list(itertools.islice(i, int(chunksize)))
        if not chunk:
            break
        yield chunk

Ich möchte nicht, dass die Funktion die Referenz auf chunk nach dem Bereitstellen behält, da sie nicht weiter verwendet wird und nur Speicher verbraucht, auch wenn alle externen Verweise verschwunden sind.


EDIT: Verwendung von Standard Python 2.5/2.6/2.7 von python.org.


Lösung (fast zeitgleich vorgeschlagen von @phihag und @Owen): Wickeln Sie das Ergebnis in ein (kleines) veränderliches Objekt und geben Sie das Chunk anonym zurück, sodass nur der kleine Container zurückbleibt:

def chunker(iterable, chunksize):
    """
    Return elements from the iterable in `chunksize`-ed lists. The last returned
    chunk may be smaller (if length of collection is not divisible by `chunksize`).

    >>> print list(chunker(xrange(10), 3))
    [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
    """
    i = iter(iterable)
    while True:
        wrapped_chunk = [list(itertools.islice(i, int(chunksize)))]
        if not wrapped_chunk[0]:
            break
        yield wrapped_chunk.pop()

Mit dieser Speicheroptimierung können Sie nun etwas wie folgt machen:

 for big_chunk in chunker(some_generator, chunksize=10000):
     ... big_chunk bearbeiten
     del big_chunk # big_chunk bereit zur Garbage Collection :-)
     ... weitere Aktionen durchführen

5 Stimmen

Häufig möchten Personen mit anderen Sprachhintergründen die Speicherzuweisung und -freigabe genau kontrollieren. Tatsächlich wird in den meisten anderen Sprachen die meiste Arbeit genau darauf ausgerichtet. Widerstehen Sie der Versuchung, Vermutungen über den Speichereffizienz eines bestimmten Python-Konstrukts anzustellen: Die Speicherverwaltung ist zu wichtig, um sie dem Programmierer zu überlassen :p

0 Stimmen

Leider brauche ich den so verschwendeten Speicher. Die Chunks sind groß. Im Moment verwende ich einen Workaround (das Schlaufenheben in den lokalen Bereich des Aufrufers, d. h. ohne eine separate Generierungsfunktion), aber ich frage mich, ob es einen besseren/saubereren Weg gibt.

1 Stimmen

Wenn Sie wirklich diese Art von Kontrolle brauchen, dann verwenden Sie die falsche Sprache. Aber in meiner Erfahrung denken die überwiegende Mehrheit der Personen, die glauben, dass sie diese Art von Kontrolle benötigen, wirklich nicht.

11voto

phihag Punkte 261131

Nach yield chunk wird der Variablenwert in der Funktion nie wieder verwendet, daher wird ein guter Interpreter/Garbage Collector chunk bereits für die Müllsammlung freigeben (Anmerkung: CPython 2.7 scheint dies nicht zu tun, Pypy 1.6 mit standardmäßigem gc). Daher müssen Sie nichts ändern, außer Ihrem Codebeispiel, das das zweite Argument an grouper fehlt.

Beachten Sie, dass die Müllsammlung in Python nicht deterministisch ist. Der Null-Garbage-Collector, der überhaupt keine freien Objekte sammelt, ist ein vollkommen gültiger Garbage-Collector. Aus dem Python-Handbuch:

Objekte werden nie explizit zerstört; jedoch können sie müllsammlungsfähig werden, wenn sie unerreichbar werden. Eine Implementierung darf die Müllsammlung verschieben oder ganz darauf verzichten – es ist eine Frage der Implementierungsqualität, wie die Müllsammlung umgesetzt ist, solange keine Objekte gesammelt werden, die noch erreichbar sind.

Daher kann nicht entschieden werden, ob ein Python-Programm Speicher "belegt" oder nicht, ohne die Python-Implementierung und den Garbage-Collector anzugeben. Bei einer bestimmten Python-Implementierung und einem Garbage-Collector können Sie das gc-Modul verwenden, um zu testen, ob das Objekt freigegeben wird.

Davon abgesehen, wenn Sie wirklich keine Referenz aus der Funktion möchten (was nicht unbedingt bedeutet, dass das Objekt für die Müllsammlung freigegeben wird), so geht das:

def grouper(iterable, chunksize):
    i = iter(iterable)
    while True:
        tmpr = [list(itertools.islice(i, int(chunksize)))]
        if not tmpr[0]:
            break
        yield tmpr.pop()

Anstelle einer Liste können Sie auch eine beliebige andere Datenstruktur verwenden, die eine Funktion mit sich bringt, die ein Objekt entfernt und zurückgibt, wie Owens Wrapper.

4voto

Owen Punkte 37648

Wenn Sie diese Funktionalität wirklich wirklich erhalten möchten, könnten Sie wahrscheinlich einen Wrapper verwenden:

class Wrap:

    def __init__(self, val):
        self.val = val

    def unlink(self):
        val = self.val
        self.val = None
        return val

Und könnte verwendet werden wie

def grouper(iterable, chunksize):
    i = iter(iterable)
    while True:
        chunk = Wrap(list(itertools.islice(i, int(chunksize))))
        if not chunk.val:
            break
        yield chunk.unlink()

Was im Wesentlichen dasselbe ist, was phihag mit pop() macht ;)

0 Stimmen

Großartig! Das Umwickeln scheint eine gute Richtung zu sein. Können Sie zeigen, wie man dieses Muster mit der grouper Funktion verwenden kann, zum Beispiel? Wenn es funktioniert, haben Sie meinen Dank (und eine Akzeptanz) :)

1 Stimmen

Schön, ich benutze tatsächlich eine ähnliche Technik (Verpackung in einem falschen Mutable) an anderer Stelle, warum bin ich nicht selbst darauf gekommen :) Es scheint, dass du zuerst dort angekommen bist, ein paar Minuten vor @phihag, und mit weniger Ablenkungen, deshalb werde ich das akzeptieren. Vielen Dank an euch beide!

2voto

eyquem Punkte 25545

@ Radim,

Einige Punkte haben mich in diesem Thread verwirrt. Ich habe bemerkt, dass ich das Grundlegende nicht verstanden habe: Was war Ihr Problem.

Jetzt denke ich, dass ich verstanden habe und ich wünsche Ihnen, dies zu bestätigen.

Ich werde Ihren Code so darstellen

import itertools

def grouper(iterable, chunksize):
    i = iter(iterable)
    while True:
        chunk = list(itertools.islice(i, int(chunksize)))
        if not chunk:
            break
        yield chunk

............
............
gigi = grouper(an_iterable,4)
# vor A
# A = grouper(an_iterable,4)
# korrigiert:
A = gigi.next()
# nach A
................
...........
# ein Objekt x aus A ableiten; x verbraucht nicht viel Speicher
............
# Löschen von A, da es viel Speicher verbraucht:
del A
# Code läuft weiter, braucht Zeit, um ausgeführt zu werden
................
................
......
..........
# vor B
# B = grouper(an_iterable,4)
# korrigiert:
B = gigi.next()
# nach B
.....................
........

Ihr Problem ist, dass selbst während der Zeit zwischen
# nach dem Löschen von A, Code läuft weiter, braucht Zeit, um ausgeführt zu werden
und
# vor B,
das Objekt mit dem gelöschten Namen 'A' immer noch existiert und viel Speicher verbraucht, weil es noch eine Bindung zwischen diesem Objekt und dem Bezeichner 'chunk' innerhalb der Generatorfunktion gibt ?

Entschuldigen Sie, dass ich Sie nach diesem jetzt offensichtlichen Punkt frage.
Da es jedoch eine gewisse Verwirrung im Thread gab, möchte ich, dass Sie bestätigen, dass ich Ihr Problem jetzt richtig verstanden habe.

.

@ phihag

Sie haben in einem Kommentar geschrieben:

1)
Nach dem yield chunk gibt es keine Möglichkeit, auf den Wert zuzugreifen der in chunk gespeichert ist. Diese Funktion hält daher keine Referenzen auf das betreffende Objekt

(Übrigens, ich hätte nicht deshalb geschrieben, sondern 'weil')

Ich denke, dass diese Behauptung #1 diskutabel ist.
Tatsächlich bin ich überzeugt, dass sie falsch ist. Aber es gibt eine Feinheit in dem, was Sie behaupten, nicht nur in diesem Zitat allein, sondern insgesamt, wenn wir auch berücksichtigen, was Sie am Anfang Ihrer Antwort sagen.

Lassen Sie uns alles ordnen.

Der folgende Code scheint das Gegenteil Ihrer Behauptung zu beweisen "Nach dem yield chunk gibt es keine Möglichkeit, auf den Wert zuzugreifen, der in chunk gespeichert ist, von dieser Funktion aus."

import itertools

def grouper(iterable, chunksize):
    i = iter(iterable)
    chunk = ''
    last = ''
    while True:
        print 'neue Runde   ',id(chunk)
        if chunk:
            last = chunk[-1]
        chunk = list(itertools.islice(i, int(chunksize)))
        print 'neuer Chunk  ',id(chunk),'  Länge des Chunks:',len(chunk)
        if not chunk:
            break
        yield '%s  -  %s' % (last,' , '.join(chunk))
        print 'Ende der Runde',id(chunk),'\n'

for x in grouper(['1','2','3','4','5','6','7','8','9','10','11'],'4'):
    print repr(x)

Ergebnis

neue Runde    10699768
neuer Chunk   18747064   Länge des Chunks: 4
'  -  1 , 2 , 3 , 4'
Ende der Runde 18747064 

neue Runde    18747064
neuer Chunk   18777312   Länge des Chunks: 4
'4  -  5 , 6 , 7 , 8'
Ende der Runde 18777312 

neue Runde    18777312
neuer Chunk   18776952   Länge des Chunks: 3
'8  -  9 , 10 , 11'
Ende der Runde 18776952 

neue Runde    18776952
neuer Chunk   18777512   Länge des Chunks: 0

.

Sie haben jedoch auch geschrieben (es ist der Anfang Ihrer Antwort):

2)
Nach yield chunk wird der Variablenwert in der Funktion nie wieder verwendet, daher wird ein guter Interpreter/Garbage Collector den chunk bereits für die Aufforderung zur Garbage Collection freigeben (Anmerkung: cpython 2.7 scheint dies nicht zu

Diesmal sagen Sie nicht, dass die Funktion nach yield chunk keine Referenz mehr auf chunk hält, sondern dass ihr Wert vor der Erneuerung von chunk in der nächsten Runde der while-Schleife nicht mehr verwendet wird. Das stimmt, im Code von Radim wird das Objekt chunk nicht wieder verwendet, bevor der Bezeichner 'chunk' in der Anweisung chunk = list(itertools.islice(i, int(chunksize))) in der nächsten Runde der Schleife neu zugewiesen wird.

.

Diese Behauptung #2 in diesem Zitat, anders als die vorherige Behauptung #1, hat zwei logische Konsequenzen:

ERSTENS, mein obiger Code kann nicht strikt beweisen, dass es tatsächlich einen Weg gibt, auf den Wert von chunk zuzugreifen, nach der Anweisung yield chunk, für jemanden, der so denkt wie Sie.
Weil die Bedingungen in meinem obigen Code nicht den Bedingungen entsprechen, unter denen Sie das Gegenteil behaupten, das heißt: im Code von Radim, über den Sie sprechen, wird das Objekt chunk tatsächlich nicht mehr verwendet, bevor die nächste Runde beginnt.
Deshalb kann man behaupten, dass es wegen der Verwendung von chunk in meinem oben genannten Code (die Anweisungen print 'Ende der Runde',id(chunk),'\n', print 'neue Runde ',id(chunk) und last = chunk[-1] verwenden es), dass eine Referenz auf das Objekt chunk noch nach dem yield chunk vorhanden ist.

ZWEITENS, wenn man weiterdenkt, führt die Kombination Ihrer beiden Zitate dazu, dass Sie denken, dass es daran liegt, dass chunk nach der yield chunk Anweisung im Code von Radim nicht mehr verwendet wird, dass keine Referenz darauf gehalten wird.
Es ist eine Frage der Logik, meiner Meinung nach: das Fehlen einer Referenz zu einem Objekt ist die Bedingung für seine Freigabe, daher wenn Sie behaupten, dass der Speicher vom Objekt befreit wird, weil es nicht mehr verwendet wird, ist es gleichbedeutend mit der Behauptung, dass der Speicher vom Objekt befreit wird, weil seine Arbeitslosigkeit den Interpreter dazu bringt, die Referenz darauf in der Funktion zu löschen.

Ich fasse zusammen:
Sie behaupten, dass in Radims Code chunk nach yield chunk nicht mehr verwendet wird, dann keine Referenz mehr darauf gehalten wird, dann ..... cpython 2.7 wird es nicht tun... aber pypy 1.6 mit standardmäßigem gc befreit den Speicher vom Objekt chunk.

Ich bin an diesem Punkt sehr überrascht über die Schlussfolgerung, und der Grund, warum ich all dem nicht zustimme, ist, dass es impliziert, dass pypy 1.6 in der Lage wäre, den Code zu analysieren und zu erkennen, dass chunk nach der Anweisung yield chunk nicht mehr verwendet wird im Code von Radim, dass keine Referenz aufrecht erhalten wird.
Es wäre meiner Meinung nach nicht klar ausgedrückt wie von Ihnen, aber ohne diesen Gedanken fände ich Ihre Behauptungen in den beiden Zitaten unlogisch und unverständlich.

Was mich an diesem Schluss verwirrt und der Grund, warum ich all dem nicht zustimme, ist, dass es impliziert, dass pypy 1.6 in der Lage wäre, den Code zu analysieren und zu erkennen, dass chunk nach der Anweisung yield chunk nicht mehr verwendet wird ein Restreferenz innerhalb der Generatorfunktion besteht. Haben Sie ein ähnliches Verhalten bei pypy 1.6 beobachtet? Ich sehe keinen anderen Weg, um die verbleibende Referenz innerhalb des Generators offenzulegen, da laut Ihrem Zitat #2 jede Verwendung von chunk nach yield chunk ausreicht, um die Aufrechterhaltung einer Referenz darauf auszulösen. Es ist ein Problem ähnlich wie in der Quantenmechanik: die Tatsache, dass die Geschwindigkeit einer Teilchens gemessen wird, verändert seine Geschwindigkeit.....

Was mich an diesem Schluss verwirrt und der Grund, warum ich all dem nicht zustimme, ist, dass es impliziert, dass pypy 1.6 in der Lage wäre, den Code zu analysieren und zu erkennen, dass chunk nach der Anweisung yield chunk nicht mehr verwendet wird ein Restreferenz innerhalb der Generatorfunktion besteht. Haben Sie ein ähnliches Verhalten bei pypy 1.6 beobachtet? Ich sehe keinen anderen Weg, um die verbleibende Referenz innerhalb des Generators offenzulegen, da laut Ihrem Zitat #2 jede Verwendung von chunk nach yield chunk ausreicht, um die Aufrechterhaltung einer Referenz darauf auszulösen. Es ist ein Problem ähnlich wie in der Quantenmechanik: die Tatsache, dass die Geschwindigkeit einer Teilchens gemessen wird, verändert seine Geschwindigkeit.....

  • Erklären Sie bitte, was Sie genau zu all dem denken. Wo liege ich falsch in meinem Verständnis Ihrer Ideen?

  • Sagen Sie, ob Sie einen Beweis dafür haben, dass zumindest pypy 1.6, keine Referenz auf chunk hält, wenn es nicht mehr verwendet wird. Was das Problem des ursprünglichen Codes von Radim war, war, dass der Speicher durch die Persistenz des Objekts chunk aufgrund seiner noch vorhandenen Referenz innerhalb der Generatorfunktion zu stark verbraucht wurde: das war ein indirektes Symptom für die Existenz einer solchen persistenten Referenz innerhalb. Haben Sie ein ähnliches Verhalten mit pypy 1.6 beobachtet? Ich sehe keinen anderen Weg, um die verbleibende Referenz innerhalb des Generators offenzulegen, da laut Ihrem Zitat #2, jede Verwendung von chunk nach yield chunk

0 Stimmen

Ja, genau. Die zusätzliche hängende Referenz von innerhalb der Funktion verhindert, dass Speicher freigegeben wird. Dein erstes Beispiel zeigt das schön (außer dass es die Chunks sind, d.h. Objekte, die von A.next() zurückgegeben werden, die hier speicherrelevant sind, nicht A selbst). Ich dachte, es sei offensichtlich, aber ich bin nicht an SO gewöhnt, ich erkenne, dass ich mit der Erklärung klarer hätte sein sollen. Die Leute springen hier sehr schnell auf Antworten, also muss man sehr sehr vorsichtig sein, um keine Möglichkeit für Missverständnisse zu geben :) Mein Fehler.

0 Stimmen

@Radim Dein Fehler ist geringfügig. Ja, es war offensichtlich und ich war dumm, das Problem nicht auf den ersten Blick zu verstehen und ein wenig beschämt, um nach Bestätigung zu fragen. Aber wie du in einem Kommentar bemerkt hast, waren die Dinge zu einem bestimmten Zeitpunkt in der Diskussion durcheinander. - Es ist eine Erleichterung, einen Benutzer zu treffen, der sich dessen bewusst ist, dass _"Leute hier sehr schnell auf Antworten kommen", _was möglicherweise zu Missverständnissen oder sogar Fehlinterpretationen führt, wobei das Schlimmste auch der Eindruck ist, dass es unmöglich ist, Klarheit zu erhalten, bevor 6 Antworten, die schnell geschrieben wurden, veröffentlicht werden. Danke.__

0 Stimmen

@Radim "es sind die Chunks, d.h. die Objekte, die von A.next() zurückgegeben werden, die hier speicherrelevant sind, nicht A selbst" Nun,.... ?? A ist das Objekt, das durch die yield-Anweisung zurückgegeben wird, das heißt eine Liste von Elementen aus dem Objekt iterierbar und eine Liste hat übrigens keine Methode next(). Du meinst, dass das Gewicht von A im Speicher tatsächlich das Gewicht seiner Elemente ist, die das eigentliche Objekt sind, da A in der Tat eine Liste von Verweisen auf diese Objekte ist? Du solltest den Begriff 'Chunks' nicht für diese Elemente verwenden, denn es ist verwirrend, da sie Elemente dessen sind, was im Generator als chunk bezeichnet wurde.

0voto

Zuerst können wir einen einzelnen Ausdruck weitergeben, der an yield übergeben wird, mit einem or-Operator, so dass, wenn list falsch ist (leer), ein alternativer Ausdruck ausgewertet wird.

def grouper(iterable, chunksize):
    i = iter(iterable)
    while True:
        yield list(itertools.islice(i, int(chunksize))) or _stop_iteration()

Die Funktion _stop_iteration() stoppt die Iteration, indem sie nicht zurückgibt und somit keinen Rückgabewert liefert. Daher wird nichts weiter ausgegeben, sobald das Eingabe-Iterable erschöpft ist. Bedenken Sie, dass eine Funktion, die nicht zurückgibt, per Definition keinen Rückgabewert liefern kann. Dies trifft auf fast jede Programmiersprache zu, die Funktionen unterstützt!

Es gibt zwei Möglichkeiten, wie eine Funktion keinen Wert liefern kann:

  1. Indem sie endlos schleift:

    def _stop_iteration_0():
        while True:
            pass

    Dies funktioniert in dem Sinne, dass nur nicht leere Listen jemals ausgegeben werden, aber die Iteration wird dann hängen bleiben. Während wir wollen, dass sie stoppt. Das sollte hoffentlich ein Licht aufgehen lassen :)

  2. Indem sie eine Ausnahme auslöst, und idealerweise eine Ausnahme, die das erreicht, was wir brauchen - nämlich das Stoppen der Iteration:

    def _stop_iteration():
        raise StopIteration()

Das ist alles, was bis Python 3.7 benötigt wird.

In Python 3.7 wird eine StopIteration-Ausnahme, die den Generator-Funktionskörper verlässt, in einen RuntimeError umgewandelt, und daher müssen wir sie selbst abfangen. So kommen wir zu der Implementierung unten, die für Python 2.5-3.9 geeignet ist:

import itertools

def _stop_iteration():
    raise StopIteration()

def grouper(iterable, chunksize):
    """
    Liefert Elemente aus dem Iterable in `chunksize`-Großen Listen zurück. Das letzte zurückgegebene Element kann kleiner sein (wenn die Länge der Sammlung nicht durch `chunksize` teilbar ist).

    >>> print(list(grouper(range(10), 3)))
    [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

    Werden keine Elemente zurückgegeben, wenn die Liste leer ist.

    >>> print(list(grouper([], 3)))
    []

    Es ist ein Fehler, den Grouper mit einer chunksize kleiner als 1 aufzurufen.

    >>> print(list(grouper([1], 0)))
    Traceback (most recent call last):
     ...
    AssertionError
    """

    assert(chunksize > 0)
    i = iter(iterable)
    try:
        while True:
            yield list(itertools.islice(i, int(chunksize))) or _stop_iteration()
    except StopIteration:
        pass

if __name__ == "__main__":
    import doctest
    doctest.testmod()

-1voto

msw Punkte 41469

Die Funktion grouper wie definiert hat den Nachteil, dass sie verschwenderische Duplikate erzeugt, weil Sie eine Funktion ohne Effekt um itertools.islice gewickelt haben. Die Lösung besteht darin, Ihren redundanten Code zu löschen.

Ich denke, es gibt Konzessionen an C-abgeleitete Sprachen, die nicht pythonisch sind und zu übermäßigem Overhead führen. Zum Beispiel haben Sie

i = iter(iterable)
itertools.islice(i)

Warum existiert i? iter wird kein Nicht-Iterable in ein Iterable umwandeln, solche Umwandlungen gibt es nicht. Wenn Sie ein Nicht-Iterable übergeben, würden beide dieser Zeilen eine Ausnahme generieren; die erste schützt nicht die zweite.

islice wird glücklicherweise als Iterator fungieren (obwohl es möglicherweise wirtschaftlicher ist als eine yield Anweisung. Sie haben zu viel Code: grouper muss wahrscheinlich nicht existieren.

0 Stimmen

Die it = iter(iterable) Technik wird aus der Dokumentation von Python für das itertools Modul kopiert und eingefügt. Was den Rest betrifft, wenn Sie eine alternative Lösung für die grouper Funktionalität vorschlagen, die funktioniert, werde ich sie gerne hochwerten :-)

0 Stimmen

Ich habe es ohne den iter-Schritt versucht, und die Unittests fangen an zu fehlen. Also ist es dort notwendig. Das wäre eigentlich eine interessante Frage an sich, willst du sie nicht auf SO posten?

0 Stimmen

Ich habe die Antwort gefunden, es ist ganz einfach: Wenn Sie islice direkt über das iterable aufrufen, beginnt es immer wieder von vorne zu iterieren (wobei jedes Mal ein neuer Iterator erstellt wird). Daher tritt das break nie auf und die Funktion bleibt einfach hängen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X