Zrozumienie generatorów w Pythonie

Czytam w tej chwili książkę kucharską Pythona i obecnie patrzę na generatory. Trudno mi się ogarnąć.

Ponieważ pochodzę z środowiska Java, czy istnieje odpowiednik Java? Książka mówi o "Producent / konsument", jednak kiedy słyszę, że myślę o threadingu.

Co to jest generator i dlaczego go używać? Oczywiście bez cytowania jakichkolwiek książek (chyba, że można znaleźć przyzwoitą, uproszczoną odpowiedź bezpośrednio z książki). Być może z przykładami, jeśli jesteś hojny!

Author: Peter Mortensen, 2009-11-18

11 answers

Uwaga: Ten post zakłada Python 3.składnia X.

A generator jest po prostu funkcją, która zwraca obiekt, na którym można wywołać next, tak że dla każdego wywołania zwraca jakąś wartość, dopóki nie wyświetli wyjątku StopIteration, sygnalizującego, że wszystkie wartości zostały wygenerowane. Taki obiekt nazywany jest iteratorem .

Normalne funkcje zwracają pojedynczą wartość za pomocą return, tak jak w Javie. W Pythonie istnieje jednak alternatywa, nazwane yield. Użycie yield gdziekolwiek w funkcji sprawia, że jest generatorem. Obserwuj ten kod:

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Jak widać, myGen(n) jest funkcją, która daje n i n + 1. Każde wezwanie do next daje pojedynczą wartość, dopóki wszystkie wartości nie zostaną uzyskane. for pętle wywołują next w tle, tak więc:

>>> for n in myGen(6):
...     print(n)
... 
6
7

Podobnie są wyrażenia generatora, które stanowią sposób na zwięzłe opisanie pewnych typowych Generatory:

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Zauważ, że wyrażenia generatora są podobneskładanie list:

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

Zauważ, że obiekt generatora jest generowany raz , ale jego kod to , a nie uruchamiany na raz. Tylko wywołania next faktycznie wykonują (część) kodu. Wykonywanie kodu w generatorze zatrzymuje się po osiągnięciu instrukcji yield, po której zwraca ona wartość. Następne wywołanie next powoduje kontynuację wykonywania w stanie w którym generator pozostał po ostatnim yield. Jest to zasadnicza różnica w przypadku funkcji regularnych: te zawsze rozpoczynają wykonywanie na "górze" i odrzucają swój stan po zwróceniu wartości.

Jest więcej rzeczy do powiedzenia na ten temat. Jest to np. możliwe, aby send dane z powrotem do generatora (odniesienie ). Ale to jest coś, co sugeruję nie patrzeć na dopóki nie zrozumiesz podstawowej koncepcji generatora.

Teraz możesz zapytać: dlaczego używać Generatory? Jest kilka dobrych powodów: {]}

  • pewne pojęcia można opisać znacznie zwięźle za pomocą generatorów.
  • zamiast tworzyć funkcję, która zwraca listę wartości, można napisać generator, który generuje wartości w locie. Oznacza to, że nie trzeba konstruować listy, co oznacza, że wynikowy kod jest bardziej wydajny w pamięci. W ten sposób można nawet opisać strumienie danych, które byłyby po prostu zbyt duże, aby zmieścić się w pamięć.
  • Generatory pozwalają w naturalny sposób opisać nieskończone strumienie. Rozważmy na przykład liczby Fibonacciego :

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    

    Ten kod używa itertools.islice aby pobrać skończoną liczbę elementów z nieskończonego strumienia. Zaleca się, aby dobrze przyjrzeć się funkcjom w itertools moduł, ponieważ są niezbędnymi narzędziami do pisania zaawansowanych generatorów z dużą łatwością.


O firmie Python W powyższych przykładach {[6] } jest funkcją, która wywołuje metodę __next__ na danym obiekcie. W Pythonie o.next() zamiast next(o). Python 2.7 ma next() wywołanie .next, więc nie musisz używać następujących w 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3
 323
Author: Stephan202,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-07 02:33:27

Generator jest efektywnie funkcją, która zwraca (dane) przed jej zakończeniem, ale zatrzymuje się w tym punkcie i można wznowić funkcję w tym punkcie.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

I tak dalej. (Lub jedną) zaletą generatorów jest to, że ponieważ zajmują się danymi po jednym kawałku na raz, możesz poradzić sobie z dużymi ilościami danych; z listami nadmierne wymagania pamięci mogą stać się problemem. Generatory, podobnie jak listy, są iteracyjne, więc mogą być używane w ten sam sposób: {]}

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Uwaga że generatory zapewniają inny sposób radzenia sobie z nieskończonością, na przykład

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

Generator zamyka nieskończoną pętlę, ale to nie jest problem, ponieważ otrzymujesz tylko każdą odpowiedź za każdym razem, gdy o nią poprosisz.

 41
Author: Caleb Hattingh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-26 14:21:02

Po pierwsze, termin generator pierwotnie był nieco źle zdefiniowany w Pythonie, co doprowadziło do wielu nieporozumień. Prawdopodobnie masz na myśli Iteratory i iterables (Zobacz tutaj). Następnie w Pythonie istnieją również funkcje generatora (które zwracają obiekt generatora), obiekty generatora (które są iteratorami) i wyrażenia generatora (które są obliczane na obiekt generatora).

Zgodnie z wpis słowniczek dla generator wydaje się, że obecnie oficjalna terminologia generator jest skrótem od "funkcji generatora". W przeszłości dokumentacja określała terminy niespójnie, ale na szczęście zostało to naprawione.

Dobrym pomysłem może być dokładność i unikanie terminu "generator" bez dalszej specyfikacji.

 23
Author: nikow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-15 05:04:20

Generatory mogą być traktowane jako skrót do tworzenia iteratora. Zachowują się jak Iterator Javy. Przykład:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Nadzieję, że to pomaga/jest to, czego szukasz.

Update:

Jak pokazuje wiele innych odpowiedzi, istnieją różne sposoby tworzenia generatora. Możesz użyć składni nawiasów, jak w moim przykładzie powyżej, lub możesz użyć yield. Inną ciekawą cechą jest to, że generatory mogą być "nieskończone" - Iteratory, które nie zatrzymują się: {]}

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...
 21
Author: overthink,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-18 14:15:52

Nie ma odpowiednika Javy.

Oto nieco wymyślony przykład:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

W generatorze jest pętla, która biegnie od 0 do n, a jeśli zmienna pętli jest wielokrotnością 3, daje zmienną.

Podczas każdej iteracji pętli for generator jest uruchamiany. Jeśli generator jest uruchamiany po raz pierwszy, zaczyna się od początku, w przeciwnym razie kontynuuje od poprzedniego czasu.

 9
Author: Wernsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-20 09:02:05

Lubię opisywać generatory, dla tych z przyzwoitym doświadczeniem w językach programowania i komputerach, pod względem ramek stosu.

W wielu językach istnieje stos, na którym znajduje się bieżąca "ramka" stosu. Ramka stosu zawiera przestrzeń przydzieloną dla zmiennych lokalnych funkcji wraz z argumentami przekazywanymi do tej funkcji.

Podczas wywoływania funkcji, bieżący punkt wykonania ("licznik programu" lub odpowiednik) jest przesuwany na stos, a tworzona jest nowa ramka stosu. Następnie wykonanie przenosi się na początek wywoływanej funkcji.

W przypadku zwykłych funkcji, w pewnym momencie funkcja zwraca wartość, a stos jest "popped". Ramka stosu funkcji jest odrzucana, a wykonanie wznawia się w poprzedniej lokalizacji.

Gdy funkcja jest generatorem, może zwrócić wartość Bez ramki stosu, używając instrukcji yield. Wartości zmiennych lokalnych oraz licznik programu w ramach funkcji są zachowane. Pozwala to na wznowienie generatora w późniejszym czasie, z kontynuacją wykonywania instrukcji yield, i może wykonać więcej kodu i zwrócić inną wartość.

Przed Pythonem 2.5 to wszystko robiły Generatory. Python 2.5 dodał również możliwość przekazywania wartości z powrotem w do generatora. W ten sposób wartość przekazywana jest dostępna jako wyrażenie wynikające ze instrukcji yield, która tymczasowo zwróciła kontrolę (oraz wartość) z generatora.

Kluczową zaletą generatorów jest to, że "stan" funkcji jest zachowany, w przeciwieństwie do zwykłych funkcji, gdzie za każdym razem, gdy ramka stosu jest odrzucana, tracisz cały ten "stan". Dodatkową zaletą jest to, że unika się niektórych funkcji wywołujących napowietrzne (tworzenie i usuwanie ramek stosu), chociaż jest to zwykle niewielka zaleta.

 7
Author: Peter Hansen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-12-19 10:50:33

Jedyną rzeczą, którą mogę dodać do odpowiedzi Stephan202 jest zalecenie, abyś spojrzał na prezentację Davida Beazleya PyCon '08 "sztuczki generatora dla programistów systemów", która jest najlepszym pojedynczym wyjaśnieniem jak i dlaczego generatorów, które widziałem wszędzie. To jest rzecz, która wzięła mnie z "Python wygląda całkiem fajnie "do" to jest to, czego szukałem."It' s at http://www.dabeaz.com/generators/.

 6
Author: Robert Rossney,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-18 17:54:00

Pozwala na wyraźne rozróżnienie między funkcją foo, a generatorem foo (n):

def foo(n):
    yield n
    yield n+1

Foo jest funkcją. foo (6) jest obiektem generatora.

Typową metodą użycia obiektu generatora jest pętla:
for n in foo(6):
    print(n)

Druki pętli

# 6
# 7
Pomyśl o generatorze jako o funkcji życiowej.

yield zachowuje się jak return w tym sensie, że otrzymane wartości są "zwracane" przez generator. W przeciwieństwie do powrotu, jednak następnym razem generator po zapytaniu o wartość, funkcja generatora, foo, wznawia pracę tam, gdzie została przerwana-po ostatniej instrukcji wydajności - i kontynuuje pracę, dopóki nie trafi innej instrukcji wydajności.

Za kulisami, kiedy wywołujesz bar=foo(6) Pasek obiektu generatora jest zdefiniowany, abyś miał atrybut next.

Możesz wywołać go samodzielnie, aby pobrać wartości uzyskane z foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Gdy foo się kończy (i nie ma już żadnych wartości), wywołanie next(bar) powoduje błąd Stopineracji.

 5
Author: unutbu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-20 09:04:37

Ten post użyje liczb Fibonacciego jako narzędzia do wyjaśnienia przydatności generatorów Pythona.

Ten post będzie zawierał zarówno kod C++, jak i Python.

Liczby Fibonacciego definiuje się jako ciąg: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....

Lub ogólnie:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
Można to bardzo łatwo przenieść do funkcji C++:]}
size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

Ale jeśli chcesz wydrukować pierwsze sześć liczb Fibonacciego, będziesz przeliczać wiele wartości za pomocą powyższej funkcji.

Na przykład: Fib(3) = Fib(2) + Fib(1), ale Fib(2) również przelicza Fib(1). Im wyższa wartość chcesz obliczyć, tym gorzej będzie.

Można więc pokusić się o przepisanie powyższego poprzez śledzenie stanu w main.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Ale To jest bardzo brzydkie, i komplikuje naszą logikę w main. Lepiej nie martwić się o stan w naszej funkcji main.

Możemy zwrócić vector z wartości i używać iterator do iteracji nad tym zestawem wartości, ale wymaga to dużo pamięci na raz dla dużej liczby zwracanych wartości.

Wracając do naszego starego podejścia, co się stanie, jeśli chcemy zrobić coś innego niż drukowanie liczb? Musielibyśmy skopiować i wkleić cały blok kodu w main i zmienić instrukcje wyjściowe na cokolwiek innego, co chcieliśmy zrobić. A jeśli skopiujesz i wkleisz kod, powinieneś zostać zastrzelony. Nie chcesz, żeby cię postrzelono, prawda?

To rozwiąż te problemy i aby uniknąć postrzelenia, możemy przepisać ten blok kodu za pomocą funkcji zwrotnej. Za każdym razem, gdy napotkamy nowy numer Fibonacciego, wywołamy funkcję callback.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Jest to oczywiście poprawa, twoja logika w main nie jest tak zaśmiecona, i możesz zrobić wszystko, co chcesz z liczbami Fibonacciego, po prostu zdefiniuj nowe wywołania zwrotne.

Ale to wciąż nie jest idealne. Co jeśli chcesz uzyskać tylko dwie pierwsze liczby Fibonacciego, a następnie zrobić coś, potem dostać więcej, a potem zrobić coś innego?

Cóż, moglibyśmy kontynuować tak, jak poprzednio, i moglibyśmy zacząć dodawać Stan ponownie do main, pozwalając GetFibNumbers zaczynać od dowolnego punktu. Ale to jeszcze bardziej nadciąć nasz kod, i już wygląda zbyt duży dla prostego zadania, takiego jak drukowanie liczb Fibonacciego.

Moglibyśmy wdrożyć model producenta i konsumenta za pomocą kilku wątków. Ale to jeszcze bardziej komplikuje kod.

Zamiast tego porozmawiajmy o generatorach.

Python ma bardzo ładną funkcję językową, która rozwiązuje problemy, takie jak te zwane generatorami.

Generator pozwala wykonać funkcję, zatrzymać się w dowolnym punkcie, a następnie kontynuować ponownie tam, gdzie przerwano. Za każdym razem zwracając wartość.

Rozważ następujący kod, który używa generatora:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Co daje nam wyniki:

0 1 1 2 3 5

Wypowiedź yield jest używana w połączenie z generatorami Pythona. Zapisuje stan funkcji i zwraca wartość yeilded. Następnym razem, gdy wywołasz funkcję next () na generatorze, będzie ona kontynuowana tam, gdzie wydajność została przerwana.

Jest to o wiele bardziej czyste niż kod funkcji zwrotnej. Mamy czystszy kod, mniejszy kod, i nie wspominając o znacznie bardziej funkcjonalny kod (Python pozwala dowolnie duże liczby całkowite).

Źródło

 4
Author: Brian R. Bondy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-20 09:19:40

Wydaje mi się, że pierwsze pojawienie się iteratorów I GENERATORÓW było w języku programowania Icon, około 20 lat temu.

Możesz cieszyć się przeglądem ikon , który pozwala Ci obejść je bez skupiania się na składni (ponieważ Icon jest językiem, którego prawdopodobnie nie znasz, a Griswold tłumaczył korzyści płynące z jego języka ludziom pochodzącym z innych języków).

Po przeczytaniu kilku akapitów, użyteczność generatorów i iteratorów może stać się bardziej widoczne.

 2
Author: Nosredna,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-18 14:53:32

Doświadczenie ze składaniem list pokazało ich powszechną użyteczność w Pythonie. Jednak wiele przypadków użycia nie musi mieć pełnej listy utworzonej w pamięci. Zamiast tego muszą tylko powtarzać elementy pojedynczo.

Na przykład poniższy kod sumujący zbuduje pełną listę kwadratów w pamięci, iteruje nad tymi wartościami, a gdy odniesienie nie jest już potrzebne, Usuń listę:

sum([x*x for x in range(10)])

Pamięć jest zachowywana za pomocą wyrażenie generatora zamiast:

sum(x*x for x in range(10))

Podobne korzyści są przyznawane konstruktorom obiektów kontenerowych:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

Wyrażenia generatora są szczególnie przydatne w funkcjach takich jak sum (), min () i max (), które redukują iterowalne dane wejściowe do pojedynczej wartości:

max(len(line)  for line in file  if line.strip())

Więcej

 2
Author: Saqib Mujtaba,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-24 18:38:33