Do czego możesz używać funkcji generatora Python?


Odpowiedzi:


239

Generatory dają leniwą ocenę. Używasz ich, iterując nad nimi, albo jawnie z „for”, albo pośrednio, przekazując je do dowolnej funkcji lub konstrukcji, która się iteruje. Możesz myśleć o generatorach jako o zwracaniu wielu elementów, tak jakby zwracały one listę, ale zamiast zwracać je wszystkie naraz, zwracają je jeden po drugim, a funkcja generatora jest wstrzymywana do momentu żądania następnego elementu.

Generatory są dobre do obliczania dużych zestawów wyników (w szczególności obliczeń obejmujących same pętle), w których nie wiesz, czy będziesz potrzebować wszystkich wyników lub gdzie nie chcesz przydzielić pamięci dla wszystkich wyników w tym samym czasie . Lub w sytuacjach, gdy generator korzysta z innego generatora lub zużywa inne zasoby, a wygodniej jest, jeśli dzieje się to tak późno, jak to możliwe.

Innym zastosowaniem generatorów (tak naprawdę jest to samo) jest zastąpienie wywołań zwrotnych iteracją. W niektórych sytuacjach chcesz, aby funkcja wykonała dużo pracy i od czasu do czasu zgłaszała się do osoby dzwoniącej. Tradycyjnie używałbyś do tego funkcji wywołania zwrotnego. Przekazujesz to wywołanie zwrotne do funkcji pracy i okresowo wywołuje to wywołanie zwrotne. Podejście oparte na generatorze polega na tym, że funkcja pracy (obecnie generator) nie wie nic o wywołaniu zwrotnym i po prostu ustępuje, gdy chce coś zgłosić. Program wywołujący, zamiast pisać osobne wywołanie zwrotne i przekazywać je do funkcji pracy, wykonuje wszystkie czynności raportowania w małej pętli „za” wokół generatora.

Załóżmy na przykład, że napisałeś program „wyszukiwania systemu plików”. Możesz przeprowadzić wyszukiwanie w całości, zebrać wyniki, a następnie wyświetlić je pojedynczo. Wszystkie wyniki musiałyby zostać zebrane przed pokazaniem pierwszego, a wszystkie wyniki byłyby w tym samym czasie w pamięci. Możesz też wyświetlać wyniki podczas ich wyszukiwania, co byłoby bardziej wydajne pod względem pamięci i bardziej przyjazne dla użytkownika. To ostatnie można wykonać, przekazując funkcję drukowania wyników do funkcji wyszukiwania systemu plików, lub można to zrobić, po prostu czyniąc funkcję wyszukiwania generatorem i iterując wynik.

Jeśli chcesz zobaczyć przykład dwóch ostatnich podejść, zobacz os.path.walk () (stara funkcja chodzenia po systemie plików z wywołaniem zwrotnym) i os.walk () (nowy generator chodzenia po systemie plików). Oczywiście, jeśli naprawdę chciałeś zebrać wszystkie wyniki na liście, podejście generatora jest trywialne, aby przekonwertować je na podejście z dużą listą:

big_list = list(the_generator)

Czy generator, taki jak ten, który tworzy listy systemów plików, wykonuje działania równolegle z kodem, który uruchamia ten generator w pętli? Idealnie byłoby, gdyby komputer uruchomił ciało pętli (przetwarzając ostatni wynik), jednocześnie wykonując wszystko, co generator musi zrobić, aby uzyskać następną wartość.
Steven Lu

@StevenLu: O ile nie ma problemu z ręcznym uruchomieniem wątków przed yieldi joinpo nich, aby uzyskać następny wynik, nie jest on wykonywany równolegle (i nie robi tego żaden standardowy generator bibliotek; potajemnie uruchamiane wątki są odrzucone). Generator zatrzymuje się przy każdym, yieldaż zostanie zażądana następna wartość. Jeśli generator pakuje operacje we / wy, system operacyjny może proaktywnie buforować dane z pliku przy założeniu, że zostanie wkrótce o to poproszony, ale taki jest system operacyjny, w którym nie jest zaangażowany Python.
ShadowRanger

90

Jednym z powodów użycia generatora jest uczynienie rozwiązania bardziej zrozumiałym dla niektórych rozwiązań.

Drugi polega na traktowaniu wyników pojedynczo, unikając tworzenia ogromnych list wyników, które i tak byłyby przetwarzane oddzielnie.

Jeśli masz funkcję Fibonacciego-up-to-n:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

Możesz łatwiej napisać funkcję w ten sposób:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

Funkcja jest bardziej przejrzysta. A jeśli użyjesz takiej funkcji:

for x in fibon(1000000):
    print x,

w tym przykładzie, jeśli używasz wersji generatora, cała lista 1000000 pozycji nie zostanie utworzona, tylko jedna wartość na raz. Nie byłoby tak w przypadku korzystania z wersji listy, w której lista byłaby tworzona jako pierwsza.


18
a jeśli potrzebujesz listy, zawsze możesz to zrobićlist(fibon(5))
endolith

41

Zobacz sekcję „Motywacja” w PEP 255 .

Nieoczywistym zastosowaniem generatorów jest tworzenie funkcji przerywalnych, które umożliwiają wykonywanie takich czynności, jak aktualizacja interfejsu użytkownika lub uruchamianie kilku zadań „jednocześnie” (w rzeczywistości z przeplotem), bez korzystania z wątków.


1
Sekcja Motywacja jest ładna, ponieważ ma konkretny przykład: „Gdy funkcja producenta ma wystarczająco ciężką pracę, która wymaga utrzymania stanu między wygenerowanymi wartościami, większość języków programowania nie oferuje przyjemnego i wydajnego rozwiązania poza dodaniem funkcji wywołania zwrotnego do argumentu producenta lista ... Na przykład tokenize.py w standardowej bibliotece przyjmuje to podejście ”
Ben Creasy 16.04.16

38

Znajduję to wyjaśnienie, które rozwiewa moje wątpliwości. Ponieważ istnieje możliwość, że osoba, która nie wie, Generatorsrównież nie wie o tymyield

Powrót

Instrukcja return to miejsce, w którym wszystkie zmienne lokalne są niszczone, a wynikowa wartość jest zwracana (zwracana) programowi wywołującemu. Jeśli jakiś czas później zostanie wywołana ta sama funkcja, funkcja otrzyma nowy zestaw zmiennych.

Wydajność

Ale co, jeśli zmienne lokalne nie zostaną wyrzucone po wyjściu z funkcji? Oznacza to, że możemy resume the functiontam, gdzie przerwaliśmy. W tym miejscu wprowadzana jest koncepcja, generatorsa yieldstwierdzenie zostaje wznowione tam, gdzie functionzostało przerwane.

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

Na tym polega różnica między returni yieldinstrukcjami w Pythonie.

Instrukcja Yield sprawia, że ​​funkcja jest funkcją generatora.

Generatory są więc prostym i wydajnym narzędziem do tworzenia iteratorów. Są napisane jak zwykłe funkcje, ale używają yieldinstrukcji, ilekroć chcą zwrócić dane. Za każdym razem, gdy wywoływana jest funkcja next (), generator wznawia pracę od miejsca, w którym został przerwany (zapamiętuje wszystkie wartości danych i ostatnią instrukcję).


33

Przykład ze świata rzeczywistego

Załóżmy, że masz 100 milionów domen w tabeli MySQL i chcesz zaktualizować pozycję Alexa dla każdej domeny.

Pierwszą rzeczą, której potrzebujesz, jest wybranie nazw domen z bazy danych.

Powiedzmy, że twoja nazwa tabeli to domainsi nazwa kolumny domain.

Jeśli użyjesz SELECT domain FROM domains, zwróci 100 milionów wierszy, co zużyje dużo pamięci. Twój serwer może ulec awarii.

Zdecydowałeś się więc uruchomić program partiami. Powiedzmy, że nasz rozmiar partii to 1000.

W naszej pierwszej partii sprawdzimy pierwsze 1000 wierszy, sprawdzimy pozycję Alexa dla każdej domeny i zaktualizujemy wiersz bazy danych.

W naszej drugiej partii będziemy pracować nad następnymi 1000 rzędami. W naszej trzeciej partii będzie to od 2001 do 3000 i tak dalej.

Teraz potrzebujemy funkcji generatora, która generuje nasze partie.

Oto nasza funkcja generatora:

def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

Jak widać, nasza funkcja stale zapisuje yieldwyniki. Jeśli użyjesz słowa kluczowego returnzamiast yield, cała funkcja zostanie zakończona, gdy osiągnie wartość return.

return - returns only once
yield - returns multiple times

Jeśli funkcja używa słowa kluczowego, yieldto jest to generator.

Teraz możesz iterować w ten sposób:

db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()

byłoby bardziej praktyczne, gdyby wydajność można było wyjaśnić w kategoriach programowania rekurencyjnego / dynamicznego!
igaurav

27

Buforowanie Gdy efektywne jest pobieranie danych w dużych porcjach, ale przetwarzanie ich w małych porcjach, generator może pomóc:

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

Powyższe pozwala łatwo oddzielić buforowanie od przetwarzania. Funkcja konsumenta może teraz tylko pobierać wartości jeden po drugim, nie martwiąc się o buforowanie.


3
Jeśli getBigChuckOfData nie jest leniwy, to nie rozumiem, jakie korzyści przynosi tutaj zysk. Jaki jest przypadek użycia tej funkcji?
Sean Geoffrey Pietz

1
Ale chodzi o to, że IIUC, buforowane pobieranie opóźnia połączenie z getBigChunkOfData. Jeśli getBigChunkOfData był już leniwy, wówczas buforowane pobieranie byłoby bezużyteczne. Każde wywołanie funkcji bufferedFetch () zwróci jeden element bufora, nawet jeśli BigChunk został już wczytany. I nie musisz jawnie liczyć następnego elementu, który ma zostać zwrócony, ponieważ mechanika wydajności robi to wprost niejawnie.
Hmijail opłakuje odrodzenie

21

Przekonałem się, że generatory są bardzo pomocne w czyszczeniu twojego kodu i dają ci bardzo unikalny sposób kapsułkowania i modularyzacji kodu. W sytuacji, gdzie trzeba coś stale wypluć wartości na podstawie własnego wewnętrznego przetwarzania A kiedy czegoś potrzebuje być wywoływana z dowolnego miejsca w kodzie (i nie tylko wewnątrz pętli lub bloku na przykład), generatory są cechą posługiwać się.

Abstrakcyjnym przykładem może być generator liczb Fibonacciego, który nie żyje w pętli, a gdy zostanie wywołany z dowolnego miejsca, zawsze zwróci następną liczbę w sekwencji:

def fib():
    first = 0
    second = 1
    yield first
    yield second

    while 1:
        next = first + second
        yield next
        first = second
        second = next

fibgen1 = fib()
fibgen2 = fib()

Teraz masz dwa obiekty generatora liczb Fibonacciego, które możesz wywoływać z dowolnego miejsca w kodzie i zawsze będą zwracać coraz większe liczby Fibonacciego w następujący sposób:

>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5

Zaletą generatorów jest to, że otaczają one stan bez konieczności przechodzenia przez obręcz tworzenia obiektów. Jednym ze sposobów myślenia o nich są „funkcje”, które zapamiętują ich stan wewnętrzny.

Mam przykład Fibonacciego z Python Generators - czym one są? a przy odrobinie wyobraźni możesz wymyślić wiele innych sytuacji, w których generatory stanowią doskonałą alternatywę dla forpętli i innych tradycyjnych konstrukcji iteracyjnych.


19

Proste wyjaśnienie: rozważ foroświadczenie

for item in iterable:
   do_stuff()

Przez większość czasu wszystkie elementy iterablenie muszą być od samego początku, ale można je generować w locie, gdy są potrzebne. W obu przypadkach może to być znacznie bardziej wydajne

  • przestrzeń (nigdy nie musisz przechowywać wszystkich przedmiotów jednocześnie) i
  • czas (iteracja może zakończyć się, zanim wszystkie elementy będą potrzebne).

Innym razem nie znasz nawet wszystkich przedmiotów przed czasem. Na przykład:

for command in user_input():
   do_stuff_with(command)

Nie masz możliwości wcześniejszego poznania wszystkich poleceń użytkownika, ale możesz skorzystać z takiej ładnej pętli, jeśli generator generuje polecenia:

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

Za pomocą generatorów można także wykonywać iteracje po nieskończonych sekwencjach, co oczywiście nie jest możliwe podczas iteracji po kontenerach.


... i nieskończoną sekwencją może być sekwencja wygenerowana przez wielokrotne przewijanie krótkiej listy, powrót do początku po osiągnięciu końca. Używam tego do wybierania kolorów na wykresach lub do tworzenia zajętych pulsatorów lub pokręteł w tekście.
Andrej Panjkov

@mataap: Jest na to itertool- patrz cycles.
martineau,

12

Moje ulubione zastosowania to operacje „filtrowania” i „zmniejszania”.

Powiedzmy, że czytamy plik i chcemy tylko linii zaczynających się od „##”.

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

Następnie możemy użyć funkcji generatora w odpowiedniej pętli

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

Przykład redukcji jest podobny. Załóżmy, że mamy plik, w którym musimy zlokalizować bloki <Location>...</Location>linii. [Nie tagi HTML, ale linie wyglądające jak tagi].

def reduceLocation( aSequence ):
    keep= False
    block= None
    for line in aSequence:
        if line.startswith("</Location"):
            block.append( line )
            yield block
            block= None
            keep= False
        elif line.startsWith("<Location"):
            block= [ line ]
            keep= True
        elif keep:
            block.append( line )
        else:
            pass
    if block is not None:
        yield block # A partial block, icky

Ponownie możemy użyć tego generatora we właściwej pętli for.

source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

Chodzi o to, że funkcja generatora pozwala nam filtrować lub redukować sekwencję, tworząc kolejną sekwencję po jednej wartości na raz.


8
fileobj.readlines()czytałby cały plik do listy w pamięci, co przeczy celowi używania generatorów. Ponieważ obiekty plików są już iterowalne, możesz for b in your_generator(fileobject):zamiast tego użyć . W ten sposób plik będzie odczytywany jeden wiersz na raz, aby uniknąć odczytu całego pliku.
nosklo

redukcja lokalizacji jest dość dziwna, jeśli chodzi o tworzenie listy, dlaczego po prostu nie dać każdej linii? Filtruj i zmniejszaj również wbudowane funkcje z oczekiwanymi zachowaniami (zobacz pomoc w ipython itp.), Użycie „zmniejsz” jest takie samo jak filtrowanie.
James Antill,

Dobra uwaga na readlines (). Zwykle zdaję sobie sprawę, że pliki są pierwszorzędnymi iteratorami linii podczas testów jednostkowych.
S.Lott,

W rzeczywistości „redukcja” łączy wiele pojedynczych linii w obiekt złożony. Dobra, to lista, ale wciąż jest to redukcja zaczerpnięta ze źródła.
S.Lott,

9

Praktycznym przykładem, w którym można skorzystać z generatora jest, jeśli masz jakiś kształt i chcesz iterować po jego rogach, krawędziach itp. Do własnego projektu ( tutaj kod źródłowy ) miałem prostokąt:

class Rect():

    def __init__(self, x, y, width, height):
        self.l_top  = (x, y)
        self.r_top  = (x+width, y)
        self.r_bot  = (x+width, y+height)
        self.l_bot  = (x, y+height)

    def __iter__(self):
        yield self.l_top
        yield self.r_top
        yield self.r_bot
        yield self.l_bot

Teraz mogę utworzyć prostokąt i zapętlić jego rogi:

myrect=Rect(50, 50, 100, 100)
for corner in myrect:
    print(corner)

Zamiast tego __iter__możesz mieć metodę iter_cornersi wywołać ją za pomocą for corner in myrect.iter_corners(). Jest po prostu bardziej elegancki w użyciu, __iter__ponieważ odtąd możemy używać nazwy instancji klasy bezpośrednio w forwyrażeniu.


Uwielbiałem pomysł przekazywania podobnych pól klasy jak generator
eusoubrasileiro,

7

Zasadniczo unika się funkcji oddzwaniania podczas iteracji nad utrzymaniem stanu wejścia.

Zobacz tutaj i tutaj, aby zobaczyć, co można zrobić za pomocą generatorów.


4

Kilka dobrych odpowiedzi tutaj, ale polecam również pełną lekturę samouczka programowania funkcjonalnego Python, który pomaga wyjaśnić niektóre z bardziej potencjalnych przypadków użycia generatorów.


3

Ponieważ nie wspomniano o metodzie wysyłania generatora, oto przykład:

def test():
    for i in xrange(5):
        val = yield
        print(val)

t = test()

# Proceed to 'yield' statement
next(t)

# Send value to yield
t.send(1)
t.send('2')
t.send([3])

Pokazuje możliwość wysłania wartości do działającego generatora. Bardziej zaawansowany kurs na temat generatorów w poniższym filmie (w tym yieldz eksploracji, generatorów do przetwarzania równoległego, przekraczania limitu rekurencji itp.)

David Beazley o generatorach na PyCon 2014


2

Używam generatorów, gdy nasz serwer internetowy działa jako serwer proxy:

  1. Klient żąda adresu serwera proxy z serwera
  2. Serwer zaczyna ładować docelowy adres URL
  3. Serwer poddaje się zwracaniu wyników klientowi, gdy tylko je otrzyma

1

Stosy rzeczy. Za każdym razem, gdy chcesz wygenerować sekwencję elementów, ale nie chcesz „zmaterializować” ich wszystkich na liście jednocześnie. Na przykład możesz mieć prosty generator, który zwraca liczby pierwsze:

def primes():
    primes_found = set()
    primes_found.add(2)
    yield 2
    for i in itertools.count(1):
        candidate = i * 2 + 1
        if not all(candidate % prime for prime in primes_found):
            primes_found.add(candidate)
            yield candidate

Następnie możesz użyć tego do wygenerowania produktów kolejnych liczb pierwszych:

def prime_products():
    primeiter = primes()
    prev = primeiter.next()
    for prime in primeiter:
        yield prime * prev
        prev = prime

Są to dość trywialne przykłady, ale możesz zobaczyć, jak może być przydatne do przetwarzania dużych (potencjalnie nieskończonych!) Zestawów danych bez generowania ich z wyprzedzeniem, co jest tylko jednym z bardziej oczywistych zastosowań.


jeśli nie jakikolwiek (kandydat% prime na pierwszą w prime_found) powinien być jeśli wszyscy (kandydat% prime na pierwszą w prime_found)
rjmunro 19.09.08

Tak, chciałem napisać „jeśli nie dowolny (kandydat% prime == 0 dla liczby pierwszej w primes_found). Ale twoja jest nieco starsza. :)
Nick Johnson

Chyba zapomniałeś usunąć „nie” z jeśli nie wszystkich (kandydat% prime na prime w primes_found)
Thava

0

Nadaje się również do drukowania liczb pierwszych do n:

def genprime(n=10):
    for num in range(3, n+1):
        for factor in range(2, num):
            if num%factor == 0:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.