Wann ist kein guter Zeitpunkt, um Python-Generatoren zu verwenden?

83

Dies ist eher die Umkehrung von Wofür können Sie Python-Generatorfunktionen verwenden? : Python-Generatoren, Generatorausdrücke und das itertoolsModul sind einige meiner Lieblingsfunktionen von Python in diesen Tagen. Sie sind besonders nützlich, wenn Sie Betriebsketten einrichten, die für einen großen Datenstapel ausgeführt werden sollen. Ich verwende sie häufig bei der Verarbeitung von DSV-Dateien.

Wann ist es also kein guter Zeitpunkt, einen Generator, einen Generatorausdruck oder eine itertoolsFunktion zu verwenden?

  • Wann sollte ich lieber zip()als itertools.izip()oder
  • range()vorbei xrange()oder
  • [x for x in foo]vorbei (x for x in foo)?

Offensichtlich müssen wir irgendwann einen Generator in tatsächliche Daten "auflösen", normalerweise indem wir eine Liste erstellen oder mit einer Nicht-Generator-Schleife darüber iterieren. Manchmal müssen wir nur die Länge kennen. Das frage ich nicht.

Wir verwenden Generatoren, damit wir keine neuen Listen für Zwischendaten im Speicher zuweisen. Dies ist insbesondere bei großen Datenmengen sinnvoll. Ist es auch für kleine Datensätze sinnvoll? Gibt es einen spürbaren Kompromiss zwischen Speicher und CPU?

Ich bin besonders interessiert, wenn jemand ein Profil erstellt hat, angesichts der aufschlussreichen Diskussion über die Leistung des Listenverständnisses im Vergleich zu map () und filter () . ( Alt Link )

David Eyk
quelle
2
Ich habe hier eine ähnliche Frage gestellt und einige Analysen durchgeführt, um festzustellen, dass Listen in meinen speziellen Beispielen für iterable Längen schneller sind<5 .
Alexander McFarlane
Beantwortet das deine Frage? Generatorausdrücke vs. Listenverständnis
ggorlen

Antworten:

55

Verwenden Sie eine Liste anstelle eines Generators, wenn:

1) Sie müssen die Daten für den Zugriff auf mehrere Male (dh Cache die Ergebnisse statt recomputing sie):

for i in outer:           # used once, okay to be a generator or return a list
    for j in inner:       # used multiple times, reusing a list is better
         ...

2) Sie benötigen einen Direktzugriff (oder einen anderen Zugriff als die sequentielle Vorwärtsreihenfolge):

for i in reversed(data): ...     # generators aren't reversible

s[i], s[j] = s[j], s[i]          # generators aren't indexable

3) Sie müssen sich verbinden Strings (die zwei Durchgänge über die Daten benötigt):

s = ''.join(data)                # lists are faster than generators in this use case

4) Sie verwenden PyPy, das den Generatorcode manchmal nicht so stark optimieren kann wie bei normalen Funktionsaufrufen und Listenmanipulationen.

Raymond Hettinger
quelle
Könnten die beiden Durchgänge für # 3 nicht vermieden werden, indem ireduceder Join repliziert wird?
Platinum Azure
Vielen Dank! Ich war mir des Verhaltens beim Verbinden von Strings nicht bewusst. Können Sie eine Erklärung liefern oder verlinken, warum zwei Durchgänge erforderlich sind?
David Eyk
5
@DavidEyk str.join addiert die Längen aller Zeichenfolgenfragmente in einem Durchgang, sodass viel Speicher für das kombinierte Endergebnis verfügbar ist. Der zweite Durchgang kopiert die Zeichenfolgenfragmente in den neuen Puffer, um eine einzelne neue Zeichenfolge zu erstellen. Siehe hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…
Raymond Hettinger
1
Interessanterweise benutze ich sehr oft Generatoren, um Ringe zu verbinden. Aber ich frage mich, wie funktioniert es, wenn es zwei Durchgänge benötigt? zum Beispiel''.join('%s' % i for i in xrange(10))
Bgusach
4
@ ikaros45 Wenn die zu verbindende Eingabe keine Liste ist, muss zusätzliche Arbeit geleistet werden, um eine temporäre Liste für die beiden Durchgänge zu erstellen. Ungefähr diese `` Daten = Daten, wenn isinstance (Daten, Liste) else list (Daten); n = Summe (Karte (Länge, Daten)); Puffer = Bytearray (n); ... <Fragmente in Puffer kopieren> `` `.
Raymond Hettinger
40

Verwenden Sie im Allgemeinen keinen Generator, wenn Sie Listenoperationen wie len (), reverse () usw. benötigen.

Es kann auch vorkommen, dass Sie keine verzögerte Auswertung wünschen (z. B. die gesamte Berechnung im Voraus durchführen, damit Sie eine Ressource freigeben können). In diesem Fall ist ein Listenausdruck möglicherweise besser.

Ryan Ginstrom
quelle
25
Wenn Sie die gesamte Berechnung im Voraus durchführen, wird sichergestellt, dass die Berechnung der Listenelemente an der Stelle ausgelöst wird, an der die Liste erstellt wird , und nicht an der Schleife, die sie anschließend durchläuft. Wenn Sie sicherstellen müssen, dass die gesamte Liste fehlerfrei verarbeitet wird, bevor Sie fortfahren, sind Generatoren nicht geeignet.
Ryan C. Thompson
4
Das ist ein guter Punkt. Es ist sehr frustrierend, die Hälfte der Verarbeitung eines Generators zu durchlaufen, nur um alles explodieren zu lassen. Es kann möglicherweise gefährlich sein.
David Eyk
26

Profil, Profil, Profil.

Das Profilieren Ihres Codes ist der einzige Weg, um festzustellen, ob das, was Sie tun, überhaupt Auswirkungen hat.

Die meisten Verwendungen von xrange, Generatoren usw. haben eine statische Größe und kleine Datensätze. Nur wenn Sie zu großen Datenmengen gelangen, macht dies wirklich einen Unterschied. Bei range () vs. xrange () geht es meistens nur darum, den Code ein bisschen hässlicher aussehen zu lassen, nichts zu verlieren und vielleicht etwas zu gewinnen.

Profil, Profil, Profil.

Jerub
quelle
1
Profil in der Tat. Eines Tages werde ich versuchen, einen empirischen Vergleich anzustellen. Bis dahin hoffte ich nur, dass es schon jemand anderes hatte. :)
David Eyk
Profil, Profil, Profil. Ich stimme vollkommen zu. Profil, Profil, Profil.
Jeppe
17

Sie sollten sich nicht begünstigen zipüber izip, rangeüber xrangeoder Listenkomprehensionen über Generator Comprehensions. In Python 3.0 rangehat xrange-ähnliche Semantik und ziphat izip-ähnliche Semantik.

Das Listenverständnis ist tatsächlich klarer, als list(frob(x) for x in foo)wenn Sie eine tatsächliche Liste benötigen.

Steven Huwig
quelle
3
@Steven Ich bin nicht anderer Meinung, aber ich frage mich, was die Gründe für Ihre Antwort sind. Warum sollten Zip-, Range- und Listenverständnisse niemals der entsprechenden "faulen" Version vorgezogen werden?
Mhawke
denn wie er sagte, wird das alte Verhalten von Reißverschluss und Reichweite bald verschwinden.
@Steven: Guter Punkt. Ich hatte diese Änderungen in 3.0 vergessen, was wahrscheinlich bedeutet, dass jemand dort oben von seiner allgemeinen Überlegenheit überzeugt ist. Betreff: Listenverständnisse, sie sind oft klarer (und schneller als erweiterte forSchleifen!), Aber man kann leicht unverständliche Listenverständnisse schreiben.
David Eyk
9
Ich verstehe, was Sie meinen, aber ich finde die []Form beschreibend genug (und prägnanter und im Allgemeinen weniger überladen). Dies ist jedoch nur eine Frage des Geschmacks.
David Eyk
4
Die Listenoperationen sind bei kleinen Datengrößen schneller, bei kleinen Datenmengen ist jedoch alles schnell. Daher sollten Sie Generatoren immer bevorzugen, es sei denn, Sie haben einen bestimmten Grund für die Verwendung von Listen (aus diesen Gründen siehe die Antwort von Ryan Ginstrom).
Ryan C. Thompson
7

Wie Sie bereits erwähnt haben: "Dies ist besonders bei großen Datenmengen sinnvoll", beantwortet dies Ihrer Meinung nach Ihre Frage.

Wenn Sie in Bezug auf die Leistung keine Wände erreichen, können Sie sich dennoch an Listen und Standardfunktionen halten. Wenn Sie dann auf Leistungsprobleme stoßen, wechseln Sie.

Wie von @ u0b34a0f6ae in den Kommentaren erwähnt, kann die Verwendung von Generatoren zu Beginn die Skalierung auf größere Datensätze erleichtern.

Mönch
quelle
5
Mit +1 Generatoren ist Ihr Code besser für große Datenmengen vorbereitet, ohne dass Sie damit rechnen müssen.
u0b34a0f6ae
6

In Bezug auf die Leistung: Wenn Sie Psyco verwenden, können Listen viel schneller sein als Generatoren. Im folgenden Beispiel sind Listen bei Verwendung von psyco.full () fast 50% schneller.

import psyco
import time
import cStringIO

def time_func(func):
    """The amount of time it requires func to run"""
    start = time.clock()
    func()
    return time.clock() - start

def fizzbuzz(num):
    """That algorithm we all know and love"""
    if not num % 3 and not num % 5:
        return "%d fizz buzz" % num
    elif not num % 3:
        return "%d fizz" % num
    elif not num % 5:
        return "%d buzz" % num
    return None

def with_list(num):
    """Try getting fizzbuzz with a list comprehension and range"""
    out = cStringIO.StringIO()
    for fibby in [fizzbuzz(x) for x in range(1, num) if fizzbuzz(x)]:
        print >> out, fibby
    return out.getvalue()

def with_genx(num):
    """Try getting fizzbuzz with generator expression and xrange"""
    out = cStringIO.StringIO()
    for fibby in (fizzbuzz(x) for x in xrange(1, num) if fizzbuzz(x)):
        print >> out, fibby
    return out.getvalue()

def main():
    """
    Test speed of generator expressions versus list comprehensions,
    with and without psyco.
    """

    #our variables
    nums = [10000, 100000]
    funcs = [with_list, with_genx]

    #  try without psyco 1st
    print "without psyco"
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

    #  now with psyco
    print "with psyco"
    psyco.full()
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

if __name__ == "__main__":
    main()

Ergebnisse:

without psyco
  number: 10000
with_list 0.0519102208309 seconds
with_genx 0.0535933367509 seconds

  number: 100000
with_list 0.542204280744 seconds
with_genx 0.557837353115 seconds

with psyco
  number: 10000
with_list 0.0286369007033 seconds
with_genx 0.0513424889137 seconds

  number: 100000
with_list 0.335414877839 seconds
with_genx 0.580363490491 seconds
Ryan Ginstrom
quelle
1
Das liegt daran, dass Psyco Generatoren überhaupt nicht beschleunigt, daher ist es eher ein Mangel von Psyco als von Generatoren. Gute Antwort.
Steven Huwig
4
Außerdem ist Psyco jetzt so gut wie nicht mehr gepflegt. Alle Entwickler verbringen Zeit mit PyPys JIT, das nach meinem besten Wissen Generatoren optimiert.
Noufal Ibrahim
3

Was die Leistung betrifft, kann ich mir keine Zeiten vorstellen, in denen Sie eine Liste über einen Generator verwenden möchten.

Jason Baker
quelle
all(True for _ in range(10 ** 8))ist langsamer als all([True for _ in range(10 ** 8)])in Python 3.8. Ich würde hier eine Liste einem Generator vorziehen
ggorlen
3

Ich habe noch nie eine Situation gefunden, in der Generatoren Ihre Versuche behindern würden. Es gibt jedoch viele Fälle, in denen die Verwendung von Generatoren Ihnen nicht mehr helfen würde, als sie nicht zu verwenden.

Beispielsweise:

sorted(xrange(5))

Bietet keine Verbesserung gegenüber:

sorted(range(5))
Jeremy Cantrell
quelle
4
Keines davon bietet eine Verbesserung gegenüber range(5), da die resultierende Liste bereits sortiert ist.
Dan04
3

Sie sollten Listenverständnisse bevorzugen, wenn Sie die Werte später für etwas anderes behalten müssen und die Größe Ihres Sets nicht zu groß ist.

Beispiel: Sie erstellen eine Liste, die Sie später in Ihrem Programm mehrmals durchlaufen werden.

In gewissem Maße können Sie sich Generatoren als Ersatz für Iteration (Schleifen) und Listenverständnis als eine Art Datenstrukturinitialisierung vorstellen. Wenn Sie die Datenstruktur beibehalten möchten, verwenden Sie Listenverständnisse.

Minze
quelle
Wenn Sie im Stream nur einen begrenzten Blick nach vorne / hinten benötigen, itertools.tee()kann Ihnen dies möglicherweise helfen. Wenn Sie jedoch mehr als einen Durchgang oder zufälligen Zugriff auf einige Zwischendaten wünschen, erstellen Sie im Allgemeinen eine Liste / einen Satz / ein Diktat daraus.
Beni Cherniavsky-Paskin