Kiedy nie jest dobry moment na używanie generatorów Pythona?

83

Jest to raczej odwrotność do czego można używać funkcji generatora w języku Python? : generatory Pythona, wyrażenia generatora i itertoolsmoduł to niektóre z moich ulubionych funkcji w dzisiejszym Pythonie. Są szczególnie przydatne podczas konfigurowania łańcuchów operacji do wykonania na dużym stosie danych - często używam ich podczas przetwarzania plików DSV.

Kiedy więc nie jest dobry moment na użycie generatora, wyrażenia generatora lub itertoolsfunkcji?

  • Kiedy wolę zip()więcej itertools.izip(), lub
  • range()ponad xrange()lub
  • [x for x in foo]koniec (x for x in foo)?

Oczywiście ostatecznie musimy „rozłożyć” generator na rzeczywiste dane, zwykle tworząc listę lub iterując po niej pętlą nie będącą generatorem. Czasami wystarczy znać długość. Nie o to proszę.

Używamy generatorów, aby nie przypisywać nowych list do pamięci dla danych tymczasowych. Ma to sens zwłaszcza w przypadku dużych zbiorów danych. Czy ma to sens także w przypadku małych zbiorów danych? Czy występuje zauważalny kompromis między pamięcią a procesorem?

Jestem szczególnie zainteresowany, jeśli ktoś zrobił jakieś profilowanie na ten temat, w świetle otwierającej oczy dyskusji na temat wydajności rozumienia list w porównaniu z mapą () i filtrem () . ( link alternatywny )

David Eyk
źródło
2
Zadałem tutaj podobne pytanie i przeprowadziłem analizę, aby stwierdzić, że w moim konkretnym przykładzie listy są szybsze dla iterowalnych długości<5 .
Alexander McFarlane
Czy to odpowiada na twoje pytanie? Wyrażenia generatora a rozumienie listy
ggorlen

Odpowiedzi:

57

Użyj listy zamiast generatora, gdy:

1) Musisz wielokrotnie uzyskiwać dostęp do danych (tj. Buforować wyniki zamiast ponownie je obliczać):

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) Potrzebujesz dostępu swobodnego (lub dowolnego dostępu innego niż kolejność do przodu):

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

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

3) Musisz połączyć ciągi znaków (co wymaga dwóch przejść przez dane):

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

4) Używasz PyPy, które czasami nie może zoptymalizować kodu generatora tak bardzo, jak przy normalnych wywołaniach funkcji i manipulowaniu listami.

Raymond Hettinger
źródło
W przypadku # 3, czy nie można było uniknąć dwóch przebiegów, używając ireducedo replikacji sprzężenia?
Platinum Azure
Dzięki! Nie byłem świadomy zachowania związanego z łączeniem strun. Czy możesz podać lub zamieścić link do wyjaśnienia, dlaczego wymaga to dwóch przejść?
David Eyk
5
@DavidEyk str.join wykonuje jeden przebieg, aby zsumować długości wszystkich fragmentów ciągu, dzięki czemu zna dużo pamięci do przydzielenia dla połączonego wyniku końcowego. Drugi przebieg kopiuje fragmenty ciągu do nowego buforu, aby utworzyć pojedynczy nowy ciąg. Zobacz hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…
Raymond Hettinger
1
Co ciekawe, bardzo często do łączenia śpiewów używam generatorów. Ale zastanawiam się, jak to działa, jeśli wymaga dwóch przejść? na przykład''.join('%s' % i for i in xrange(10))
bgusach
4
@ ikaros45 Jeśli wejście do przyłączenia nie jest listą, musi wykonać dodatkową pracę, aby zbudować tymczasową listę dla dwóch przebiegów. Z grubsza to `` dane = dane, jeśli jest instancja (dane, lista) else lista (dane); n = suma (mapa (len, dane)); bufor = bytearray (n); ... <skopiuj fragmenty do bufora> ``.
Raymond Hettinger
40

Ogólnie rzecz biorąc, nie używaj generatora, gdy potrzebujesz operacji listowych, takich jak len (), reverse () i tak dalej.

Może się również zdarzyć, że nie chcesz leniwej oceny (np. Wykonanie wszystkich obliczeń z góry, abyś mógł zwolnić zasób). W takim przypadku wyrażenie listy może być lepsze.

Ryan Ginstrom
źródło
25
Ponadto wykonanie wszystkich obliczeń z góry zapewnia, że ​​jeśli obliczenie elementów listy zgłosi wyjątek, zostanie on wyrzucony w miejscu, w którym lista jest tworzona , a nie w pętli, która następnie ją iteruje. Jeśli chcesz zapewnić bezbłędne przetwarzanie całej listy przed kontynuowaniem, generatory nie są dobre.
Ryan C. Thompson
4
Trafne spostrzeżenie. Bardzo frustrujące jest przejście w połowie przetwarzania generatora tylko po to, aby wszystko eksplodowało. Może to być potencjalnie niebezpieczne.
David Eyk
26

Profil, profil, profil.

Profilowanie kodu to jedyny sposób, aby dowiedzieć się, czy to, co robisz, ma jakikolwiek wpływ.

Większość zastosowań xrange, generatorów itp. Dotyczy rozmiaru statycznego, małych zbiorów danych. Dopiero gdy uzyskasz duże zbiory danych, to naprawdę robi różnicę. range () vs. xrange () to głównie kwestia tego, aby kod wyglądał odrobinę bardziej brzydko i niczego nie tracił, a może coś zyskał.

Profil, profil, profil.

Jerub
źródło
1
Profil rzeczywiście. Któregoś dnia spróbuję przeprowadzić porównanie empiryczne. Do tego czasu miałam nadzieję, że ktoś inny już to zrobił. :)
David Eyk
Profil, profil, profil. Całkowicie się zgadzam. Profil, profil, profil.
Jeppe,
17

Nigdy nie powinno się faworyzować zipna izip, rangenad xrangelub listowych ponad listowe generatora. W Pythonie 3.0 rangema xrangesemantykę podobną do-i semantykę podobną do zipma izip.

Listy składane są w rzeczywistości bardziej przejrzyste, jak wtedy, list(frob(x) for x in foo)gdy potrzebujesz rzeczywistej listy.

Steven Huwig
źródło
3
@Steven Nie zgadzam się, ale zastanawiam się, jaki jest powód Twojej odpowiedzi. Dlaczego wyrażenia typu zip, range i list nie powinny być nigdy faworyzowane w stosunku do odpowiadających im "leniwych" wersji?
mhawke
ponieważ, jak powiedział, stare zachowanie zip i zasięgu wkrótce zniknie.
@Steven: Słuszna uwaga. Zapomniałem o tych zmianach w 3.0, co prawdopodobnie oznacza, że ​​ktoś tam na górze jest przekonany o ich ogólnej wyższości. Re: Listy składane, często są jaśniejsze (i szybsze niż rozwinięte forpętle!), Ale łatwo można napisać niezrozumiałe listy składane.
David Eyk
9
Rozumiem, co masz na myśli, ale uważam, że []formularz jest wystarczająco opisowy (i ogólnie bardziej zwięzły i mniej zagracony). Ale to tylko kwestia gustu.
David Eyk
4
Operacje na listach są szybsze w przypadku małych rozmiarów danych, ale wszystko przebiega szybko, gdy rozmiar danych jest mały, dlatego zawsze powinieneś preferować generatory, chyba że masz konkretny powód, aby używać list (z takich powodów zobacz odpowiedź Ryana Ginstroma).
Ryan C. Thompson
7

Jak wspomniałeś, „To ma sens zwłaszcza w przypadku dużych zbiorów danych”, myślę, że to odpowiada na twoje pytanie.

Jeśli nie uderzasz w żadne ściany, jeśli chodzi o wydajność, nadal możesz trzymać się list i standardowych funkcji. Następnie, gdy napotkasz problemy z wydajnością, dokonaj zmiany.

Jak wspomniał @ u0b34a0f6ae w komentarzach, jednak użycie generatorów na początku może ułatwić skalowanie do większych zbiorów danych.

monkut
źródło
5
Generatory +1 sprawiają, że Twój kod jest bardziej gotowy na duże zbiory danych bez konieczności przewidywania tego.
u0b34a0f6ae
6

Jeśli chodzi o wydajność: jeśli używasz psyco, listy mogą być nieco szybsze niż generatory. W poniższym przykładzie listy są prawie 50% szybsze przy użyciu psyco.full ()

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()

Wyniki:

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
źródło
1
Dzieje się tak dlatego, że psyco w ogóle nie przyspiesza generatorów, więc jest to raczej wada psyco niż generatorów. Dobra odpowiedź.
Steven Huwig
4
Ponadto psyco jest teraz prawie nieużywany. Wszyscy programiści spędzają czas na JIT PyPy, który zgodnie z moją najlepszą wiedzą optymalizuje generatory.
Noufal Ibrahim
3

Jeśli chodzi o wydajność, nie przychodzi mi do głowy żaden moment, w którym chciałbyś użyć listy zamiast generatora.

Jason Baker
źródło
all(True for _ in range(10 ** 8))jest wolniejszy niż all([True for _ in range(10 ** 8)])w Pythonie 3.8. Wolałbym listę niż generator tutaj
ggorlen
3

Nigdy nie znalazłem sytuacji, w której generatory utrudniałyby to, co próbujesz zrobić. Jest jednak wiele przypadków, w których użycie generatorów nie pomogłoby bardziej niż ich nieużywanie.

Na przykład:

sorted(xrange(5))

Nie oferuje żadnej poprawy w stosunku do:

sorted(range(5))
Jeremy Cantrell
źródło
4
Żadne z nich nie oferuje żadnej poprawy range(5), ponieważ wynikowa lista jest już posortowana.
dan04
3

Powinieneś preferować wyrażenia listowe, jeśli chcesz później zachować wartości dla czegoś innego, a rozmiar twojego zestawu nie jest zbyt duży.

Na przykład: tworzysz listę, którą będziesz powtarzać kilka razy później w swoim programie.

Do pewnego stopnia można myśleć o generatorach jako o zamienniku dla iteracji (pętli), a o listach składanych jako o typie inicjalizacji struktury danych. Jeśli chcesz zachować strukturę danych, użyj wyrażeń listowych.

miętowy
źródło
Jeśli potrzebujesz tylko ograniczonego patrzenia w przyszłość / wstecz na strumień, być może itertools.tee()może ci pomóc. Ale ogólnie, jeśli chcesz więcej niż jeden przebieg lub losowy dostęp do niektórych danych pośrednich, zrób ich listę / ustaw / dyktuj.
Beni Cherniavsky-Paskin