Wyrażenia generatora a zrozumienie listy

411

Kiedy należy używać wyrażeń generatora, a kiedy używać wyrażeń list w Pythonie?

# Generator expression
(x*2 for x in range(256))

# List comprehension
[x*2 for x in range(256)]
Tylko czytać
źródło
27
może [exp for x in iter]być po prostu cukier list((exp for x in iter))? czy jest różnica w wykonaniu?
b0fh
1
wydaje mi się, że miałem odpowiednie pytanie, więc kiedy używamy wydajności, czy możemy użyć tylko wyrażenia generatora z funkcji, czy też musimy użyć wydajności dla funkcji, aby zwrócić obiekt generatora?
28
@ b0fh Bardzo późna odpowiedź na twój komentarz: w Python2 jest niewielka różnica, zmienna pętli wycieknie ze zrozumienia listy, a wyrażenie generatora nie wycieknie. Porównaj X = [x**2 for x in range(5)]; print xz Y = list(y**2 for y in range(5)); print y, drugi da błąd. W Python3 rozumienie listy jest rzeczywiście składniowym cukrem dla wyrażenia generatora dostarczanego list()zgodnie z oczekiwaniami, więc zmienna pętli nie będzie już wyciekać .
Bas Swinckels
12
Sugerowałbym przeczytanie PEP 0289 . Podsumowując: „Ten PEP wprowadza wyrażenia generatorów jako wysokowydajne, wydajne pod względem pamięci uogólnienie wyrażeń listowych i generatorów” . Zawiera także użyteczne przykłady użycia.
icc97
5
@ icc97 Jestem również osiem lat spóźniony na imprezę, a link PEP był doskonały. Dziękujemy za ułatwienie znalezienia!
eenblam

Odpowiedzi:

283

Odpowiedź Johna jest dobra (ta lista jest lepsza, gdy chcesz powtórzyć coś wiele razy). Warto jednak zauważyć, że powinieneś użyć listy, jeśli chcesz użyć dowolnej z metod listy. Na przykład następujący kod nie będzie działać:

def gen():
    return (something for something in get_some_stuff())

print gen()[:2]     # generators don't support indexing or slicing
print [5,6] + gen() # generators can't be added to lists

Zasadniczo użyj wyrażenia generatora, jeśli wszystko, co robisz, to iteracja raz. Jeśli chcesz przechowywać wygenerowane wyniki i korzystać z nich, prawdopodobnie lepiej jest zapoznać się z listą.

Ponieważ wydajność jest najczęstszym powodem wyboru jednego spośród drugiego, radzę nie martwić się o to i po prostu wybrać jedno; jeśli okaże się, że twój program działa zbyt wolno, wtedy i tylko wtedy powinieneś wrócić i martwić się o dostrojenie kodu.

Eli Courtwright
źródło
70
Czasami trzeba mieć , aby korzystać z generatorów - na przykład, jeśli piszesz współprogram z spółdzielnia szeregowanie z wykorzystaniem wydajność. Ale jeśli to robisz, prawdopodobnie nie zadajesz tego pytania;)
ephemient
12
Wiem, że to stare, ale myślę, że warto zauważyć, że generatory (i dowolne iterowalne) można dodawać do list z rozszerzeniem: a = [1, 2, 3] b = [4, 5, 6] a.extend(b)- a będzie teraz [1, 2, 3, 4, 5, 6]. (Czy możesz dodawać nowe wiersze w komentarzach?)
jarvisteve
12
@jarvisteve twój przykład przeczy słowom, które wypowiadasz. Tu też jest sens. Listy można rozszerzyć o generatory, ale wtedy nie było sensu tworzyć z nich generatora. Generatory nie mogą być rozszerzane o listy, a generatory nie są do końca iterowalne. a = (x for x in range(0,10)), b = [1,2,3]na przykład. a.extend(b)zgłasza wyjątek. b.extend(a)oceni wszystko, w którym to przypadku nie ma sensu tworzyć generatora.
Slater Victoroff,
4
@SlaterTyranus jesteś w 100% poprawny, i głosowałem cię za dokładność. niemniej jednak uważam, że jego komentarz jest użyteczną odpowiedzią na pytanie PO, ponieważ pomoże tym, którzy się tu znajdą, ponieważ wpisali coś w rodzaju „połącz generator z analizą listy” w wyszukiwarce.
rbp
1
Czy powód użycia generatora do iteracji raz (np. Moja obawa o brak pamięci przesłania moją obawy o „pobieranie” wartości pojedynczo ) prawdopodobnie nadal miałaby zastosowanie przy wielokrotnym iterowaniu? Powiedziałbym, że może to uczynić listę bardziej przydatną, ale to, czy to wystarczy, aby przeważyć problemy związane z pamięcią, to coś innego.
Rob Grant
181

Iteracja wyrażeniem generatora lub zrozumieniem listy zrobi to samo. Jednak zrozumienie listy utworzy najpierw całą listę w pamięci, podczas gdy wyrażenie generatora utworzy elementy w locie, dzięki czemu będziesz mógł używać jej do bardzo dużych (a także nieskończonych!) Sekwencji.

dF.
źródło
39
+1 za nieskończoność. Nie możesz tego zrobić z listą, niezależnie od tego, jak mało zależy ci na wydajności.
Paul Draper,
Czy potrafisz tworzyć nieskończone generatory za pomocą metody rozumienia?
AnnanFay
5
@Annan Tylko jeśli masz już dostęp do innego nieskończonego generatora. Na przykład, itertools.count(n)jest nieskończoną sekwencją liczb całkowitych, zaczynającą się od n, więc (2 ** item for item in itertools.count(n))byłaby nieskończoną sekwencją mocy 2rozpoczynających się od 2 ** n.
Kevin
2
Generator usuwa elementy z pamięci po ich iteracji. Jest więc szybki, jeśli masz duże dane, które chcesz na przykład wyświetlić. To nie świnia pamięci. za pomocą generatorów elementy są przetwarzane „w razie potrzeby”. jeśli chcesz się zawiesić na liście lub powtórzyć iterację (więc przechowuj elementy), skorzystaj ze zrozumienia listy.
j2emanue
102

Używaj wyrażeń z listy, gdy wynik musi być wielokrotnie powtarzany lub gdy szybkość jest najważniejsza. Używaj wyrażeń generatora, gdy zakres jest duży lub nieskończony.

Aby uzyskać więcej informacji, zobacz Wyrażenia generatora i opisy list .

John Millikin
źródło
2
Będzie to prawdopodobnie trochę nie na temat, ale niestety „nie można googlować” ... Co w tym kontekście oznaczałoby „nadrzędne”? Nie jestem rodzimym językiem angielskim ... :)
Guillermo Ares
6
@ Guillermo: Jest to bezpośredni wynik „googlowania” w znaczeniu nadrzędnym: ważniejsze niż cokolwiek innego; najwyższy.
Sнаđошƒаӽ
1
Czy więc listssą szybsze niż generatorwyrażenia? Po przeczytaniu odpowiedzi dF okazało się, że jest na odwrót.
Hassan Baig
1
Prawdopodobnie lepiej powiedzieć, że zrozumienie listy jest szybsze, gdy zasięg jest mały, ale wraz ze wzrostem skali staje się bardziej wartościowe obliczanie wartości w locie - w sam raz na ich użycie. To właśnie robi wyrażenie generatora.
Kyle,
59

Ważną kwestią jest to, że analiza listy tworzy nową listę. Generator tworzy iterowalny obiekt, który „filtruje” materiał źródłowy „w locie”, gdy zużywasz bity.

Wyobraź sobie, że masz plik dziennika o wielkości 2 TB o nazwie „ogromny_plik.txt” i potrzebujesz zawartości i długości wszystkich wierszy rozpoczynających się od słowa „WEJŚCIE”.

Więc zacznij od napisania listy ze zrozumieniem:

logfile = open("hugefile.txt","r")
entry_lines = [(line,len(line)) for line in logfile if line.startswith("ENTRY")]

Spowalnia to cały plik, przetwarza każdą linię i przechowuje pasujące linie w tablicy. Ta tablica może zatem zawierać do 2 TB treści. To dużo pamięci RAM i prawdopodobnie nie jest praktyczne dla twoich celów.

Zamiast tego możemy użyć generatora, aby zastosować „filtr” do naszych treści. Żadne dane nie są odczytywane, dopóki nie zaczniemy iteracji nad wynikiem.

logfile = open("hugefile.txt","r")
entry_lines = ((line,len(line)) for line in logfile if line.startswith("ENTRY"))

Nawet żaden wiersz nie został jeszcze odczytany z naszego pliku. Powiedzmy, że chcemy jeszcze bardziej filtrować nasz wynik:

long_entries = ((line,length) for (line,length) in entry_lines if length > 80)

Wciąż nic nie zostało przeczytane, ale określiliśmy teraz dwa generatory, które będą działały na naszych danych, jak chcemy.

Wypiszmy nasze przefiltrowane linie do innego pliku:

outfile = open("filtered.txt","a")
for entry,length in long_entries:
    outfile.write(entry)

Teraz czytamy plik wejściowy. Ponieważ nasza forpętla nadal żąda dodatkowych linii, long_entriesgenerator żąda linii od entry_linesgeneratora, zwracając tylko te, których długość jest większa niż 80 znaków. Z kolei entry_linesgenerator żąda wierszy (filtrowanych jak wskazano) z logfileiteratora, który z kolei odczytuje plik.

Dlatego zamiast „wypychać” dane do funkcji wyjściowej w postaci w pełni wypełnionej listy, dajesz funkcji wyjściowej sposób „wyciągania” danych tylko wtedy, gdy jest to potrzebne. W naszym przypadku jest to o wiele bardziej wydajne, ale nie tak elastyczne. Generatory są jednokierunkowe, jedno przejście; dane z odczytanego pliku dziennika są natychmiast odrzucane, więc nie możemy wrócić do poprzedniej linii. Z drugiej strony nie musimy martwić się o przechowywanie danych, gdy skończymy.

tylerl
źródło
46

Zaletą wyrażenia generatora jest to, że zużywa mniej pamięci, ponieważ nie buduje całej listy jednocześnie. Wyrażeń generatora najlepiej używać, gdy lista jest pośrednikiem, takim jak sumowanie wyników lub tworzenie dykta z wyników.

Na przykład:

sum(x*2 for x in xrange(256))

dict( (k, some_func(k)) for k in some_list_of_keys )

Zaletą jest to, że lista nie jest w pełni generowana, a zatem używana jest niewielka pamięć (i powinna być również szybsza)

Powinieneś jednak używać wyrażeń listowych, gdy pożądanym produktem końcowym jest lista. Nie będziesz zapisywać żadnych pamięci za pomocą wyrażeń generatora, ponieważ chcesz wygenerowaną listę. Korzyścią jest także możliwość korzystania z dowolnej z funkcji listy, takich jak sortowanie lub odwracanie.

Na przykład:

reversed( [x*2 for x in xrange(256)] )
Głaskanie pod brodę
źródło
9
W języku tym znajduje się wskazówka, że ​​wyrażenia generatora powinny być używane w ten sposób. Strać nawiasy! sum(x*2 for x in xrange(256))
u0b34a0f6ae
8
sortedi reverseddziała dobrze na dowolnych iterowalnych wyrażeniach generatora.
marr75
1
Jeśli możesz użyć wersji 2.7 i nowszych, ten przykład dict () wyglądałby lepiej jako rozumienie dict (PEP jest w tym przypadku starszy niż wyrażenia PEP generatora, ale wylądował dłużej)
Jürgen A. Erhard
14

Podczas tworzenia generatora ze zmiennego obiektu (takiego jak lista) należy pamiętać, że generator zostanie oceniony na podstawie stanu listy w momencie korzystania z generatora, a nie w momencie tworzenia generatora:

>>> mylist = ["a", "b", "c"]
>>> gen = (elem + "1" for elem in mylist)
>>> mylist.clear()
>>> for x in gen: print (x)
# nothing

Jeśli istnieje szansa, że ​​twoja lista zostanie zmodyfikowana (lub zmienny obiekt na tej liście), ale potrzebujesz stanu przy tworzeniu generatora, musisz zamiast tego użyć interpretacji listy.

dziwak
źródło
1
I to powinna być zaakceptowana odpowiedź. Jeśli twoje dane są większe niż dostępna pamięć, zawsze powinieneś używać generatorów, chociaż przewijanie listy w pamięci może być szybsze (ale nie masz wystarczającej ilości pamięci, aby to zrobić).
Marek Marczak
4

Czasami możesz uciec od funkcji tee z itertools , zwraca ona wiele iteratorów dla tego samego generatora, z których można korzystać niezależnie.

Jacob Rigby
źródło
4

Używam modułu Hadoop Mincemeat . Myślę, że to świetny przykład na zanotowanie:

import mincemeat

def mapfn(k,v):
    for w in v:
        yield 'sum',w
        #yield 'count',1


def reducefn(k,v): 
    r1=sum(v)
    r2=len(v)
    print r2
    m=r1/r2
    std=0
    for i in range(r2):
       std+=pow(abs(v[i]-m),2)  
    res=pow((std/r2),0.5)
    return r1,r2,res

Tutaj generator pobiera liczby z pliku tekstowego (nawet 15 GB) i stosuje prostą matematykę na tych liczbach, korzystając z funkcji zmniejszania map przez Hadoop. Gdybym nie użył funkcji plonu, ale zamiast zrozumienia listy, obliczenie sum i średniej zajęłoby znacznie więcej czasu (nie wspominając o złożoności przestrzeni).

Hadoop jest doskonałym przykładem wykorzystania wszystkich zalet generatorów.

Murphy
źródło