Do czego służy słowo kluczowe „dochód”?

10184

Jakie jest użycie yieldsłowa kluczowego w Pythonie i co robi?

Na przykład próbuję zrozumieć ten kod 1 :

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

A to jest osoba dzwoniąca:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Co się stanie, gdy _get_child_candidateszostanie wywołana metoda ? Czy lista jest zwracana? Pojedynczy element? Czy to się nazywa ponownie? Kiedy zatrzymają się kolejne połączenia?


1. Ten fragment kodu został napisany przez Jochena Schulza (jrschulz), który stworzył świetną bibliotekę Pythona dla przestrzeni metrycznych. To jest link do pełnego źródła: moduł mspace .

Alex. S.
źródło

Odpowiedzi:

14637

Aby zrozumieć, co yieldrobi, musisz zrozumieć, czym są generatory . I zanim zrozumiesz generatory, musisz zrozumieć iterowalne .

Iterables

Kiedy tworzysz listę, możesz odczytywać jej elementy jeden po drugim. Czytanie poszczególnych pozycji jeden po drugim nazywa się iteracją:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylistjest iterowalny . Kiedy używasz rozumienia listy, tworzysz listę, a więc iterowalną:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

Wszystko, czego możesz użyć na „ for... in...”, jest iterowalne; lists, strings, Pliki ...

Te iteracje są przydatne, ponieważ możesz je odczytać tyle, ile chcesz, ale wszystkie wartości przechowujesz w pamięci i nie zawsze jest to potrzebne, gdy masz wiele wartości.

Generatory

Generatory są iteratorami, rodzajem iteracji, które można iterować tylko raz . Generatory nie przechowują wszystkich wartości w pamięci, generują je w locie :

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

Jest tak samo, z wyjątkiem tego, że użyłeś ()zamiast []. ALE nie możesz wykonać for i in mygeneratordrugiego razu, ponieważ generatory mogą być używane tylko raz: obliczają 0, następnie zapominają o tym i obliczają 1, i kończą obliczanie 4, jeden po drugim.

Wydajność

yieldjest słowem kluczowym, które jest używane podobnie return, ale funkcja zwróci generator.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

Oto bezużyteczny przykład, ale jest przydatny, gdy wiesz, że twoja funkcja zwróci ogromny zestaw wartości, które będziesz musiał odczytać tylko raz.

Aby opanować yield, musisz zrozumieć, że po wywołaniu funkcji kod napisany w treści funkcji nie jest uruchamiany. Funkcja zwraca tylko obiekt generatora, jest to nieco trudne :-)

Następnie kod będzie kontynuowany od miejsca, w którym został przerwany za każdym razem, gdy forużywa generatora.

Teraz najtrudniejsza część:

Przy pierwszym forwywołaniu obiektu generatora utworzonego z twojej funkcji, uruchomi on kod w twojej funkcji od początku aż do trafienia yield, a następnie zwróci pierwszą wartość pętli. Następnie każde kolejne wywołanie uruchomi kolejną iterację pętli zapisanej w funkcji i zwróci następną wartość. Będzie to trwało, dopóki generator nie zostanie uznany za pusty, co dzieje się, gdy funkcja działa bez uderzenia yield. Może to być spowodowane tym, że pętla dobiegła końca lub dlatego, że nie spełniasz już warunku "if/else".


Twój kod został wyjaśniony

Generator:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

Gość:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

Ten kod zawiera kilka inteligentnych części:

  • Pętla iteruje się na liście, ale lista rozszerza się podczas iteracji :-) Jest to zwięzły sposób na przejrzenie wszystkich tych zagnieżdżonych danych, nawet jeśli jest to trochę niebezpieczne, ponieważ możesz skończyć z nieskończoną pętlą. W takim przypadku candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))wyczerp wszystkie wartości generatora, ale whilenadal tworzy nowe obiekty generatora, które będą generować inne wartości niż poprzednie, ponieważ nie są one stosowane w tym samym węźle.

  • extend()Metoda jest lista metoda obiekt, który oczekuje iterable i dodaje swoje wartości do listy.

Zwykle podajemy do niej listę:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

Ale w kodzie dostaje generator, co jest dobre, ponieważ:

  1. Nie musisz czytać wartości dwa razy.
  2. Możesz mieć dużo dzieci i nie chcesz, aby wszystkie były przechowywane w pamięci.

I działa, ponieważ Python nie dba o to, czy argumentem metody jest lista, czy nie. Python oczekuje iterable, więc będzie działał z ciągami, listami, krotkami i generatorami! Nazywa się to pisaniem kaczek i jest jednym z powodów, dla których Python jest taki fajny. Ale to już inna historia, na inne pytanie ...

Możesz się tu zatrzymać lub poczytać, aby zobaczyć zaawansowane zastosowanie generatora:

Kontrolowanie wyczerpania generatora

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

Uwaga: W przypadku Python 3 użyj print(corner_street_atm.__next__())lubprint(next(corner_street_atm))

Może to być przydatne do różnych rzeczy, takich jak kontrolowanie dostępu do zasobu.

Itertools, twój najlepszy przyjaciel

Moduł itertools zawiera specjalne funkcje do manipulowania iteracjami. Czy kiedykolwiek chciałeś zduplikować generator? Łańcuch dwóch generatorów? Czy grupować wartości na zagnieżdżonej liście za pomocą jednego wiersza? Map / Zipbez tworzenia kolejnej listy?

Więc po prostu import itertools.

Przykład? Zobaczmy możliwe rozkazy przylotów dla wyścigu czterech koni:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

Zrozumienie wewnętrznych mechanizmów iteracji

Iteracja jest procesem polegającym na iteracjach (implementacja __iter__()metody) i iteratorach (implementacja __next__()metody). Iterabile to dowolne obiekty, z których można uzyskać iterator. Iteratory to obiekty, które pozwalają iterować na iterowalnych.

Więcej informacji na ten temat można znaleźć w tym artykule na temat działania forpętli .

e-satis
źródło
353
yieldta odpowiedź nie jest tak magiczna. Kiedy wywołujesz funkcję, która zawiera yieldinstrukcję w dowolnym miejscu, otrzymujesz obiekt generatora, ale nie działa żaden kod. Następnie za każdym razem, gdy wyodrębniasz obiekt z generatora, Python wykonuje kod w funkcji, aż dojdzie do yieldinstrukcji, a następnie zatrzymuje się i dostarcza obiekt. Kiedy wyodrębniasz inny obiekt, Python wznawia się zaraz po nim yieldi kontynuuje, aż osiągnie inny yield(często ten sam, ale jedna iteracja później). Trwa to do momentu, gdy funkcja przestanie działać, w tym momencie generator zostanie uznany za wyczerpany.
Matthias Fripp
28
„Te iteratory są przydatne ... ale przechowujesz wszystkie wartości w pamięci i nie zawsze to jest to, czego chcesz”, jest złe lub mylące. An iterowalny Zwraca iterator po wywołaniu ITER () na iterable i iterator nie zawsze musi przechowywać swoje wartości w pamięci, w zależności od wykonania iter metody, może również generować wartości w sekwencji na żądanie.
picmate 涅
Byłoby miło dodać do tej wspaniałej odpowiedzi, dlaczego jest tak samo, z wyjątkiem tego, że użyłeś ()zamiast[] , a konkretnie tego, co ()jest (może wystąpić pomyłka z krotką).
WoJ
Mogę się mylić, ale generator nie jest iteratorem, „zwany generator” to iterator.
aderchox
2006

Skrót do zrozumienia yield

Gdy zobaczysz funkcję z yieldinstrukcjami, zastosuj tę prostą sztuczkę, aby zrozumieć, co się stanie:

  1. Wstaw linię result = []na początku funkcji.
  2. Wymień każdy yield exprz result.append(expr).
  3. Wstaw linię return resultna dole funkcji.
  4. Tak - nigdy więcej yieldoświadczeń! Przeczytaj i wymyśl kod.
  5. Porównaj funkcję z oryginalną definicją.

Ta sztuczka może dać ci wyobrażenie o logice funkcji, ale o tym, co się właściwie dzieje yield różni się znacznie od tego, co dzieje się w podejściu opartym na liście. W wielu przypadkach metoda wydajności będzie znacznie wydajniejsza i szybsza. W innych przypadkach ta sztuczka utknie w nieskończonej pętli, mimo że oryginalna funkcja działa dobrze. Czytaj dalej, aby dowiedzieć się więcej ...

Nie myl swoich Iterabajtów, Iteratorów i Generatorów

Po pierwsze, protokół iteratora - podczas pisania

for x in mylist:
    ...loop body...

Python wykonuje następujące dwa kroki:

  1. Pobiera iterator dla mylist:

    Wywołanie iter(mylist)-> zwraca obiekt z next()metodą (lub __next__()w Pythonie 3).

    [To jest krok, o którym większość ludzi zapomina ci powiedzieć]

  2. Używa iteratora do zapętlania elementów:

    Nadal wywołuj next()metodę na iteratorze zwróconą z kroku 1. Zwracana wartość z next()jest przypisywana do xciała pętli i wykonywane. Jeśli wyjątek StopIterationzostanie zgłoszony od wewnątrz next(), oznacza to, że w iteratorze nie ma już więcej wartości i pętla zostaje zamknięta.

Prawda jest taka, że ​​Python wykonuje powyższe dwa kroki za każdym razem, gdy chce zapętlić zawartość obiektu - może to być pętla for, ale może to być również kod otherlist.extend(mylist)(gdzie otherlistjest lista Pythona).

Tutaj mylistjest iterable ponieważ implementuje protokół iteratora. W klasie zdefiniowanej przez użytkownika można zaimplementować tę __iter__()metodę, aby instancje klasy były iterowalne. Ta metoda powinna zwrócić iterator . Iterator to obiekt z next()metodą. Możliwe jest zaimplementowanie obu __iter__()i next()na tej samej klasie i __iter__()zwrot self. Będzie to działać w prostych przypadkach, ale nie wtedy, gdy chcesz, aby dwa iteratory zapętlały ten sam obiekt w tym samym czasie.

To jest protokół iteratora, wiele obiektów implementuje ten protokół:

  1. Wbudowane listy, słowniki, krotki, zestawy, pliki.
  2. Klasy zdefiniowane przez użytkownika, które implementują __iter__().
  3. Generatory

Zauważ, że forpętla nie wie, z jakim obiektem ma do czynienia - po prostu postępuje zgodnie z protokołem iteratora i chętnie otrzymuje element po elemencie, gdy wywołuje next(). Wbudowane listy zwracają swoje elementy jeden po drugim, słowniki zwracają klucze jeden po drugim, pliki zwracają wiersze jeden po drugim itd. I generatory zwracają ... cóż, to yieldjest miejsce:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Zamiast yieldinstrukcji, gdybyś miał trzy returninstrukcje f123()tylko pierwsza byłaby wykonana, a funkcja by się zakończyła. Ale f123()to nie jest zwykła funkcja. Kiedy f123()jest wywoływany, nie zwraca żadnej wartości z instrukcji return! Zwraca obiekt generatora. Ponadto funkcja tak naprawdę nie wychodzi - przechodzi w stan zawieszenia. Gdy forpętla próbuje zapętlić obiekt generatora, funkcja wznawia działanie ze stanu zawieszenia w następnym wierszu po yieldpoprzednio zwróconym, wykonuje następny wiersz kodu, w tym przypadku yieldinstrukcję, i zwraca to jako następny pozycja. Dzieje się tak, dopóki funkcja nie wyjdzie, w którym momencie generator się podnosi StopIteration, a pętla nie wychodzi.

Tak więc obiekt generatora przypomina coś w rodzaju adaptera - na jednym końcu pokazuje protokół iteratora, ujawniając __iter__()i next()metody utrzymywania forszczęśliwej pętli. Z drugiej strony jednak uruchamia funkcję na tyle, aby uzyskać z niej następną wartość i przełącza ją z powrotem w tryb zawieszony.

Dlaczego warto korzystać z generatorów?

Zwykle możesz pisać kod, który nie używa generatorów, ale implementuje tę samą logikę. Jedną z opcji jest użycie wspomnianej wcześniej „sztuczki” z listy tymczasowej. To nie zadziała we wszystkich przypadkach, na przykład jeśli masz nieskończone pętle lub może nieefektywnie wykorzystywać pamięć, gdy masz naprawdę długą listę. Drugim podejściem jest zaimplementowanie nowej klasy iterowalnej SomethingIter, która utrzymuje stan w elementach instancji i wykonuje kolejny logiczny krok w metodzie it next()(lub __next__()w Pythonie 3). W zależności od logiki kod wewnątrz next()metody może wyglądać na bardzo skomplikowany i podatny na błędy. Tutaj generatory zapewniają czyste i łatwe rozwiązanie.

użytkownik 28409
źródło
19
„Gdy zobaczysz funkcję z instrukcjami dotyczącymi wydajności, zastosuj tę prostą sztuczkę, aby zrozumieć, co się stanie”. Czy to nie całkowicie ignoruje fakt, że można przejśćsend do generatora, który jest ogromną częścią sedna generatorów?
DanielSank
10
„może to być pętla for, ale może to być również kod podobny do otherlist.extend(mylist)” -> To jest niepoprawne. extend()modyfikuje listę w miejscu i nie zwraca iterowalności. Próba zapętlenia zakończy otherlist.extend(mylist)się niepowodzeniem, TypeErrorponieważ extend()niejawnie powraca Nonei nie można zapętlić None.
Pedro
4
@pedro Źle zrozumiałeś to zdanie. Oznacza to, że podczas wykonywania Python wykonuje dwa wymienione kroki na mylist(nie włączony otherlist) otherlist.extend(mylist).
dziś
555

Pomyśl o tym w ten sposób:

Iterator to tylko fantazyjnie brzmiący termin dla obiektu, który ma next()metodę. Tak więc funkcja wydajna kończy się mniej więcej tak:

Orginalna wersja:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

Jest to w zasadzie to, co interpreter Pythona robi z powyższym kodem:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

Aby uzyskać więcej wglądu w to, co dzieje się za kulisami, forpętlę można przepisać na:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

Czy to ma więcej sensu, czy tylko bardziej dezorientuje? :)

Powinienem zauważyć, że jest to nadmierne uproszczenie w celach ilustracyjnych. :)

Jason Baker
źródło
1
__getitem__można zdefiniować zamiast __iter__. Na przykład class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i)
:,
16
Próbowałem tego przykładu w Pythonie 3.6 i jeśli tworzę iterator = some_function(), zmienna iteratornie ma już funkcji o nazwie next(), ale tylko __next__()funkcję. Myślałem, że o tym wspomnę.
Peter
Gdzie fornapisana przez Ciebie implementacja pętli wywołuje __iter__metodę iteratorinstancji instancji it?
SystematicDisintegration
455

Słowo yieldkluczowe ogranicza się do dwóch prostych faktów:

  1. Jeśli kompilator wykryje yieldsłowo kluczowe w dowolnym miejscu funkcji, funkcja ta nie zwraca już returninstrukcji. Zamiast tego natychmiast zwraca leniwy obiekt „listy oczekujących” zwany generatorem
  2. Generator jest iterowalny. Co to jest iterowalny ? To wszystko jest jak widok listlub setlub rangewidok dykta, z wbudowanym protokołem do odwiedzania każdego elementu w określonej kolejności .

W skrócie: generator jest leniwą, rosnącą listą , a yieldinstrukcje pozwalają na użycie notacji funkcji do zaprogramowania wartości listy, które generator powinien stopniowo wypluć.

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

Przykład

Zdefiniujmy funkcję makeRangepodobną do Pythona range. Wywoływanie makeRange(n)ZWRACA GENERATOR:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

Aby zmusić generator do natychmiastowego zwrócenia oczekujących wartości, możesz przekazać go do list()(tak jak możesz to zrobić w dowolnej iteracji):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

Porównywanie przykładu z „po prostu zwracaniem listy”

Powyższy przykład można traktować jako tworzenie listy, do której dołączasz i którą zwracasz:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

Jest jednak jedna zasadnicza różnica; patrz ostatnia sekcja.


Jak możesz korzystać z generatorów

Iterowalny jest ostatnią częścią zrozumienia listy, a wszystkie generatory są iterowalne, więc często używa się ich w ten sposób:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

Aby lepiej poznać generatory, możesz pobawić się itertoolsmodułem (pamiętaj, aby używać chain.from_iterableraczej niż chainwtedy, gdy jest to uzasadnione). Na przykład możesz nawet użyć generatorów, aby zaimplementować nieskończenie długie leniwe listy, takie jak itertools.count(). Możesz zaimplementować własne def enumerate(iterable): zip(count(), iterable)lub alternatywnie zrobić to za pomocą yieldsłowa kluczowego w pętli while.

Uwaga: generatory mogą być używane do wielu innych rzeczy, takich jak implementacja coroutines lub programowania niedeterministycznego lub innych eleganckich rzeczy. Jednak punkt widzenia „leniwych list”, który tu prezentuję, jest najczęstszym zastosowaniem, jaki można znaleźć.


Za kulisami

Tak działa „protokół iteracyjny Pythona”. To znaczy, co się dzieje, kiedy to robisz list(makeRange(5)). To właśnie opisałem wcześniej jako „leniwą, przyrostową listę”.

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Wbudowana funkcja next()po prostu wywołuje funkcję obiektów .next(), która jest częścią „protokołu iteracji” i znajduje się na wszystkich iteratorach. Możesz ręcznie użyć next()funkcji (i innych części protokołu iteracji), aby zaimplementować wymyślne rzeczy, zwykle kosztem czytelności, więc staraj się tego nie robić ...


Drobne szczegóły

Zwykle większość ludzi nie przejmowałaby się następującymi wyróżnieniami i prawdopodobnie chciałaby przestać tutaj czytać.

W Python-speak iterowalnym jest każdy obiekt, który „rozumie pojęcie pętli for”, taki jak lista [1,2,3], a iterator jest specyficzną instancją żądanej pętli for [1,2,3].__iter__(). Generator jest dokładnie taki sam jak każdy iteratora, z wyjątkiem sposób został napisany (ze składnią funkcji).

Gdy poprosisz o iterator z listy, tworzy on nowy iterator. Jednak gdy poprosisz o iterator od iteratora (co rzadko byś robił), po prostu daje ci kopię samego siebie.

Dlatego w mało prawdopodobnym przypadku, gdy nie robisz czegoś takiego ...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... pamiętaj więc, że generator jest iteratorem ; to jest jednorazowe użycie. Jeśli chcesz go ponownie użyć, zadzwoń myRange(...)ponownie. Jeśli musisz użyć wyniku dwa razy, przekonwertuj wynik na listę i zapisz go w zmiennej x = list(myRange(5)). Osoby, które absolutnie trzeba sklonować generator (na przykład, którzy robią przerażająco hackish metaprogramowanie) można użyć itertools.tee, jeśli jest to absolutnie konieczne, ponieważ copyable iterator Python PEP standardy propozycja została odroczona.

ninjagecko
źródło
377

Co yieldsłowo kluczowe robi w Pythonie?

Odpowiedź Konspekt / Podsumowanie

  • Funkcja yield, po wywołaniu, zwraca generator .
  • Generatory są iteratorami, ponieważ implementują protokół iteratora , dzięki czemu można iterować nad nimi.
  • Generatorowi można również przesyłać informacje , co czyni go konceptualnie korupcją .
  • W Pythonie 3 możesz delegować z jednego generatora na drugi w obu kierunkach za pomocą yield from.
  • (Dodatek krytykuje kilka odpowiedzi, w tym pierwszą i omawia użycie returnw generatorze.)

Generatory:

yieldjest legalne tylko w definicji funkcji, a włączenie yielddo definicji funkcji powoduje, że zwraca generator.

Pomysł na generatory pochodzi z innych języków (patrz przypis 1) z różnymi implementacjami. W Generatorach Pythona wykonanie kodu jest zawieszane w punkcie wydajności. Po wywołaniu generatora (metody omówiono poniżej) wykonanie zostaje wznowione, a następnie zawiesza się przy następnej wydajności.

yieldzapewnia łatwy sposób implementacji protokołu iteratora , zdefiniowanego za pomocą dwóch następujących metod: __iter__oraz next(Python 2) lub __next__(Python 3). Obie te metody sprawiają, że obiekt jest iteratorem, który można sprawdzić za pomocą Iteratorabstrakcyjnej klasy bazowej z collectionsmodułu.

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

Generator jest podtypem iteratora:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

Jeśli to konieczne, możemy wpisać:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Cechą tego jest Iterator to, że po wyczerpaniu nie można go ponownie użyć ani zresetować:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

Będziesz musiał zrobić kolejny, jeśli chcesz ponownie skorzystać z jego funkcjonalności (patrz przypis 2):

>>> list(func())
['I am', 'a generator!']

Można uzyskać dane programowo, na przykład:

def func(an_iterable):
    for item in an_iterable:
        yield item

Powyższy prosty generator jest również równoważny z poniższym - od wersji Python 3.3 (i niedostępnej w Python 2) możesz użyć yield from:

def func(an_iterable):
    yield from an_iterable

Jednak yield fromzezwala również na delegowanie do podrzędnych generatorów, co zostanie wyjaśnione w poniższej części dotyczącej delegowania kooperacyjnego z podkorupami.

Coroutines:

yield tworzy wyrażenie, które pozwala na przesłanie danych do generatora (patrz przypis 3)

Oto przykład, zwróć uwagę na receivedzmienną, która będzie wskazywać na dane wysyłane do generatora:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

Po pierwsze, musimy stać w kolejce do generatora za pomocą funkcji wbudowanej, next. Wywoła odpowiednią metodę nextlub __next__metodę, w zależności od używanej wersji Pythona:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

A teraz możemy wysłać dane do generatora. ( Wysyłanie Nonejest takie samo jak dzwonienienext .):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

Delegacja kooperacyjna w Sub-Coroutine z yield from

Przypomnijmy, że yield fromjest dostępny w Pythonie 3. To pozwala nam delegować coroutines do podprogramu:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

A teraz możemy przekazać funkcjonalność subgeneratorowi i może on być używany przez generator tak jak powyżej:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

Możesz przeczytać więcej o dokładnej semantyce yield fromw PEP 380.

Inne metody: zamknij i rzuć

closeMetoda budzi GeneratorExitw punkcie wykonanie funkcja została zamrożona. Zostanie to również wywołane __del__, abyś mógł umieścić dowolny kod czyszczenia w miejscu, w którym wykonujesz GeneratorExit:

>>> my_account.close()

Możesz także zgłosić wyjątek, który można obsłużyć w generatorze lub przekazać użytkownikowi:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

Wniosek

Uważam, że omówiłem wszystkie aspekty następującego pytania:

Co yieldsłowo kluczowe robi w Pythonie?

Okazuje się, że yielddużo. Jestem pewien, że mógłbym dodać do tego jeszcze dokładniejsze przykłady. Jeśli chcesz więcej lub masz konstruktywną krytykę, daj mi znać, komentując poniżej.


Dodatek:

Krytyka najpopularniejszej / zaakceptowanej odpowiedzi **

  • Jest mylone z tym, co czyni iterowalną , po prostu używając listy jako przykładu. Zobacz moje referencje powyżej, ale w skrócie: iterable ma __iter__metodę zwracającą iterator . Iterator zapewnia .next(Python 2 lub .__next__3 metody (Python), który jest niejawnie wywoływana przez forpętle aż podnosi StopIteration, a gdy tak się stanie, to będzie nadal to robić.
  • Następnie używa wyrażenia generatora, aby opisać, czym jest generator. Ponieważ generator jest po prostu wygodnym sposobem na utworzenie iteratora , to tylko myli sprawę, a my wciąż nie doszliśmy do sedna yield.
  • W Sterowanie wyczerpanie generator on wywołuje .nextmetodę, gdy zamiast powinien używać funkcji wbudowanego, next. Byłaby to odpowiednia warstwa pośrednicząca, ponieważ jego kod nie działa w Pythonie 3.
  • Itertools? Nie miało to znaczenia yieldw ogóle.
  • Brak dyskusji na temat metod yieldudostępnianych wraz z nową funkcjonalnością yield fromw Pythonie 3. Górna / zaakceptowana odpowiedź jest bardzo niepełną odpowiedzią.

Krytyka odpowiedzi sugerująca yieldw wyrażeniu lub zrozumieniu generatora.

Gramatyka obecnie pozwala na dowolne wyrażanie się w zrozumieniu listy.

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

Ponieważ wydajność jest wyrażeniem, niektórzy reklamują ją jako interesującą w użyciu w zrozumieniu lub wyrażeniu generatora - pomimo braku cytowania szczególnie dobrego przypadku użycia.

Główni programiści CPython dyskutują nad wycofaniem tego limitu . Oto odpowiedni post z listy mailingowej:

30 stycznia 2017 o 19:05 Brett Cannon napisał:

W niedzielę, 29 stycznia 2017 o 16:39 Craig Rodrigues napisał:

Z każdym podejściem jestem w porządku. Pozostawienie rzeczy takimi, jakimi są w Pythonie 3, nie jest dobre, IMHO.

Mój głos brzmi: błąd składni, ponieważ nie otrzymujesz tego, czego oczekujesz od składni.

Zgadzam się, że to rozsądne miejsce dla nas, ponieważ każdy kod oparty na obecnym zachowaniu jest naprawdę zbyt sprytny, aby można go było utrzymać.

Jeśli chodzi o dotarcie tam, prawdopodobnie będziemy chcieli:

  • SkładniaWarning lub DeprecationWarning w 3.7
  • Ostrzeżenie Py3k w 2.7.x
  • Błąd składni w 3.8

Na zdrowie, Nick.

- Nick Coghlan | ncoghlan na gmail.com | Brisbane, Australia

Ponadto istnieje nierozstrzygnięty problem (10544), który wydaje się wskazywać, że nigdy nie jest to dobry pomysł (PyPy, implementacja Pythona napisana w Pythonie, już wywołuje ostrzeżenia składniowe).

Podsumowując, dopóki programiści CPython nie powiedzą nam inaczej: Nie wpisuj yieldwyrażenia ani zrozumienia generatora.

returnOświadczenie w generatorze

W Python 2 :

W funkcji generatora returninstrukcja nie może zawierać znaku expression_list. W tym kontekście gołe returnoznacza, że ​​generator jest gotowy i spowoduje StopIterationpodniesienie.

An expression_listto w zasadzie dowolna liczba wyrażeń oddzielonych przecinkami - zasadniczo w Pythonie 2 możesz zatrzymać generator za pomocą return, ale nie możesz zwrócić wartości.

W Pythonie 3 :

W funkcji generatora returninstrukcja wskazuje, że generator jest gotowy i spowoduje StopIterationpodniesienie. Zwrócona wartość (jeśli istnieje) jest używana jako argument do skonstruowania StopIterationi staje się StopIteration.valueatrybutem.

Przypisy

  1. Języki CLU, Sather i Icon zostały przywołane w propozycji wprowadzenia koncepcji generatorów do Pythona. Ogólna idea jest taka, że ​​funkcja może utrzymywać stan wewnętrzny i generować pośrednie punkty danych na żądanie użytkownika. Zapowiadało się, że będzie lepsza pod względem wydajności niż inne podejścia, w tym wątki Pythona , które nie są nawet dostępne w niektórych systemach.

  2. Oznacza to na przykład, że xrangeobiekty ( rangew Pythonie 3) nie są obiektami Iterator, nawet jeśli są iterowalne, ponieważ można je ponownie wykorzystać. Podobnie jak listy, ich __iter__metody zwracają obiekty iteratora.

  3. yieldzostał pierwotnie wprowadzony jako instrukcja, co oznacza, że ​​może pojawić się tylko na początku wiersza w bloku kodu. Teraz yieldtworzy wyrażenie wydajności. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Ta zmiana została zaproponowana, aby umożliwić użytkownikowi wysyłanie danych do generatora dokładnie tak, jak można go otrzymać. Aby wysłać dane, trzeba umieć je do czegoś przypisać, a do tego oświadczenie po prostu nie zadziała.

Aaron Hall
źródło
328

yieldjest jak return- zwraca wszystko, co mu powiesz (jako generator). Różnica polega na tym, że przy następnym wywołaniu generatora wykonanie rozpoczyna się od ostatniego wywołania yieldinstrukcji. W przeciwieństwie do return, ramka stosu nie jest czyszczona, gdy pojawia się wydajność, jednak kontrola jest przenoszona z powrotem do programu wywołującego, więc jego stan zostanie wznowiony przy następnym wywołaniu funkcji.

W przypadku twojego kodu funkcja get_child_candidatesdziała jak iterator, więc kiedy rozszerzasz listę, dodaje jeden element na raz do nowej listy.

list.extendwywołuje iterator, aż zostanie wyczerpany. W przypadku opublikowanego przykładu kodu znacznie łatwiej byłoby po prostu zwrócić krotkę i dołączyć ją do listy.

Douglas Mayle
źródło
107
To jest blisko, ale nie jest poprawne. Za każdym razem, gdy wywołujesz funkcję z instrukcją return, zwraca ona zupełnie nowy obiekt generatora. Tylko po wywołaniu metody .next () tego generatora wykonanie jest wznawiane po ostatniej wydajności.
kurosch
239

Jest jeszcze jedna rzecz, o której należy wspomnieć: funkcja, która daje wartość, nie musi tak naprawdę kończyć się. Napisałem taki kod:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

Następnie mogę użyć go w innym kodzie:

for f in fib():
    if some_condition: break
    coolfuncs(f);

To naprawdę pomaga uprościć niektóre problemy i ułatwia pewne rzeczy.

Claudiu
źródło
233

Dla tych, którzy preferują minimalny działający przykład, rozważ medytację podczas tej interaktywnej sesji Pythona:

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print(i)
... 
1
2
3
>>> for i in g:
...   print(i)
... 
>>> # Note that this time nothing was printed
Daniel
źródło
208

TL; DR

Zamiast tego:

def square_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

Zrób to:

def square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Ilekroć zaczynasz budować listę od zera, yieldzamiast tego każdy kawałek.

To był mój pierwszy „aha” moment z plonem.


yieldto słodki sposób na powiedzenie

zbudować szereg rzeczy

To samo zachowanie:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

Różne zachowanie:

Wydajność jest jednoprzebiegowa : możesz iterować tylko raz. Gdy funkcja ma w sobie wydajność, nazywamy ją funkcją generatora . I zwraca iterator . Te warunki są odkrywcze. Tracimy wygodę kontenera, ale zyskujemy moc serii, która jest obliczana w razie potrzeby i dowolnie długa.

Wydajność jest leniwa , odkłada obliczenia. Funkcja z wydajnością wcale nie jest wykonywana, gdy ją wywołujesz. Zwraca obiekt iteratora, który pamięta, gdzie został przerwany. Za każdym razem, gdy wywołujesz next()iterator (dzieje się tak w pętli for), wykonanie jest o kilka cali do następnej wydajności. returnpodnosi StopIteration i kończy serię (jest to naturalny koniec pętli for).

Wydajność jest wszechstronna . Dane nie muszą być przechowywane razem, mogą być udostępniane pojedynczo. Może być nieskończony.

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

Jeśli potrzebujesz wielu przejść, a seria nie jest zbyt długa, po prostu ją wywołaj list():

>>> list(square_yield(4))
[0, 1, 4, 9]

Świetny wybór tego słowa, yieldponieważ obowiązują oba znaczenia :

plon - produkuj lub dostarczaj (jak w rolnictwie)

... podaj kolejne dane z serii.

wydajność - ustępuj lub porzuć (jak we władzy politycznej)

... porzuć wykonywanie procesora, dopóki iterator nie posunie się naprzód.

Bob Stein
źródło
194

Wydajność daje ci generator.

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

Jak widać, w pierwszym przypadku foo cała lista jest jednocześnie w pamięci. To nie jest wielka sprawa dla listy z 5 elementami, ale co, jeśli chcesz listę 5 milionów? Jest to nie tylko ogromny zjadacz pamięci, ale także kosztuje dużo czasu w momencie wywołania tej funkcji.

W drugim przypadku barpo prostu daje ci generator. Generator jest iterowalny - co oznacza, że ​​można go używać w forpętli itp., Ale do każdej wartości można uzyskać tylko raz. Wszystkie wartości nie są jednocześnie zapisywane w pamięci; obiekt generatora „zapamiętuje” miejsce, w którym znajdował się w pętli przy ostatnim wywołaniu - w ten sposób, jeśli używasz iterowalnego (powiedzmy) do policzenia do 50 miliardów, nie musisz liczyć do 50 miliardów wszystkich od razu i zapisz 50 miliardów liczb do przeliczenia.

Ponownie, jest to dość wymyślony przykład, prawdopodobnie użyłbyś narzędzi itertool, jeśli naprawdę chciałbyś liczyć do 50 miliardów. :)

Jest to najprostszy przypadek użycia generatorów. Jak już powiedziałeś, można go użyć do napisania wydajnych permutacji, wykorzystując fedrunek do wypychania rzeczy przez stos wywołań, zamiast używać jakiejś zmiennej stosu. Generatory mogą być również używane do specjalistycznego przechodzenia przez drzewa i wszelkiego rodzaju innych rzeczy.

RBansal
źródło
Tylko uwaga - w Pythonie 3 rangerównież zwraca generator zamiast listy, więc zobaczysz również podobny pomysł, z tym wyjątkiem, że __repr__/ __str__są zastępowane, aby pokazać ładniejszy wynik, w tym przypadku range(1, 10, 2).
It'sNotALie.
189

Zwraca generator. Nie jestem szczególnie zaznajomiony z Pythonem, ale uważam, że jest to ten sam rodzaj bloków iteratora C #, jeśli znasz je.

Kluczową ideą jest to, że kompilator / interpreter / cokolwiek robi pewne sztuczki, aby w odniesieniu do wywołującego mogli oni dalej wywoływać next () i nadal zwracali wartości - tak jakby metoda generatora została wstrzymana . Teraz oczywiście nie można tak naprawdę „wstrzymać” metody, więc kompilator buduje maszynę stanów, aby zapamiętać, gdzie się aktualnie znajdujesz i jak wyglądają zmienne lokalne itp. Jest to o wiele łatwiejsze niż samodzielne pisanie iteratora.

Jon Skeet
źródło
167

Jest jeden rodzaj odpowiedzi, która nie wydaje mi się, że została jeszcze udzielona, ​​spośród wielu wspaniałych odpowiedzi opisujących sposób korzystania z generatorów. Oto odpowiedź teorii języka programowania:

yieldOświadczenie w Pythonie zwraca generator. Generator w Pythonie jest funkcją, która zwraca kontynuacje (a konkretnie rodzaj coroutine, ale kontynuacje reprezentują bardziej ogólny mechanizm rozumienia, co się dzieje).

Kontynuacja teorii języków programowania jest znacznie bardziej fundamentalnym rodzajem obliczeń, ale nie jest często używana, ponieważ są one bardzo trudne do uzasadnienia, a także bardzo trudne do wdrożenia. Jednak idea kontynuacji jest prosta: jest to stan obliczeń, który jeszcze się nie zakończył. W tym stanie są zapisywane bieżące wartości zmiennych, operacje, które jeszcze nie zostały wykonane itd. Następnie w pewnym momencie w programie można wywołać kontynuację, tak aby zmienne programu zostały zresetowane do tego stanu i wykonane zostały zapisane operacje.

Kontynuacje, w tej bardziej ogólnej formie, można realizować na dwa sposoby. W ten call/ccsposób stos programu jest dosłownie zapisywany, a następnie przy wywołaniu kontynuacji stos jest przywracany.

W stylu kontynuacji przekazywania (CPS) kontynuacje są po prostu normalnymi funkcjami (tylko w językach, w których funkcje są pierwszej klasy), którymi programista wyraźnie zarządza i przekazuje podprogramy. W tym stylu stan programu jest reprezentowany przez zamknięcia (i zmienne, które się w nich zakodowały), a nie zmienne, które znajdują się gdzieś na stosie. Funkcje zarządzające przepływem sterowania przyjmują kontynuację jako argumenty (w niektórych odmianach CPS funkcje mogą przyjmować wiele kontynuacji) i manipulują przepływem sterowania, wywołując je, po prostu wywołując je i zwracając później. Bardzo prosty przykład stylu przekazywania kontynuacji jest następujący:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

W tym (bardzo uproszczonym) przykładzie programista zapisuje operację zapisu pliku do kontynuacji (która może być bardzo złożoną operacją z wieloma szczegółami do zapisania), a następnie przekazuje tę kontynuację (tj. Jako pierwszy- zamknięcie klasy) innemu operatorowi, który wykonuje więcej przetwarzania, a następnie wywołuje je, jeśli to konieczne. (Często używam tego wzorca projektowego w rzeczywistym programowaniu GUI, ponieważ oszczędza mi on wiersze kodu lub, co ważniejsze, zarządza przepływem sterowania po uruchomieniu zdarzeń GUI.)

Pozostała część tego postu, bez utraty ogólności, będzie konceptualizować kontynuacje jako CPS, ponieważ jest o wiele łatwiejsze do zrozumienia i przeczytania.


Porozmawiajmy teraz o generatorach w Pythonie. Generatory są szczególnym podtypem kontynuacji. Podczas gdy kontynuacje są w stanie ogólnie zapisać stan obliczeń (tj. Stos wywołań programu), generatory są w stanie zapisać stan iteracji tylko przez iterator . Chociaż ta definicja jest nieco myląca dla niektórych przypadków użycia generatorów. Na przykład:

def f():
  while True:
    yield 4

Jest to wyraźnie uzasadniona iteracja, której zachowanie jest dobrze określone - za każdym razem, gdy generator iteruje nad nią, zwraca 4 (i robi to na zawsze). Ale prawdopodobnie nie jest to prototypowy typ iterowalności, który przychodzi na myśl, gdy myślimy o iteratorach (tj for x in collection: do_something(x).). Ten przykład ilustruje moc generatorów: jeśli cokolwiek jest iteratorem, generator może zapisać stan iteracji.

Powtórzmy: Kontynuacje mogą zapisać stan stosu programu, a generatory mogą zapisać stan iteracji. Oznacza to, że kontynuacje są o wiele potężniejsze niż generatory, ale także, że generatory są dużo, dużo łatwiejsze. Są one łatwiejsze do zaimplementowania przez projektanta języka i są łatwiejsze dla programisty (jeśli masz trochę czasu na nagranie, spróbuj przeczytać i zrozumieć tę stronę o kontynuacjach i wywołaniu / cc ).

Ale można łatwo wdrożyć (i konceptualizować) generatory jako prosty, konkretny przypadek stylu przekazywania kontynuacji:

Ilekroć yieldjest wywoływane, mówi funkcji, aby zwróciła kontynuację. Gdy funkcja zostanie ponownie wywołana, zaczyna się od momentu jej zakończenia. Tak więc w pseudo-pseudokodzie (tj. Nie pseudokodzie, ale nie kodzie) nextmetoda generatora jest zasadniczo następująca:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

gdzie yieldsłowem kluczowym jest faktycznie cukier składniowy dla prawdziwej funkcji generatora, w zasadzie coś takiego:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

Pamiętaj, że jest to tylko pseudokod, a faktyczna implementacja generatorów w Pythonie jest bardziej złożona. Ale jako ćwiczenie, aby zrozumieć, co się dzieje, spróbuj użyć stylu przekazywania kontynuacji do implementacji obiektów generatora bez użycia yieldsłowa kluczowego.

aestrivex
źródło
152

Oto przykład w prostym języku. Zapewnię zgodność między wysokopoziomowymi ludzkimi koncepcjami a niskopoziomowymi koncepcjami Python.

Chcę operować na sekwencji liczb, ale nie chcę zawracać sobie głowy tworzeniem tej sekwencji, chcę skupić się tylko na operacji, którą chcę wykonać. Więc wykonuję następujące czynności:

  • Wzywam cię i mówię, że chcę ciąg liczb, który jest tworzony w określony sposób, i daję znać, jaki jest algorytm.
    Ten krok odpowiada defwprowadzeniu funkcji generatora, tj. Funkcji zawierającej a yield.
  • Jakiś czas później powiem ci: „OK, przygotuj się, by powiedzieć mi sekwencję liczb”.
    Ten krok odpowiada wywołaniu funkcji generatora, która zwraca obiekt generatora. Pamiętaj, że nie podałeś mi jeszcze żadnych liczb; po prostu chwytasz papier i ołówek.
  • Proszę cię: „powiedz mi następny numer”, a ty powiedz mi pierwszy numer; potem zaczekaj, aż poproszę cię o kolejny numer. Twoim zadaniem jest zapamiętanie, gdzie byłeś, jakie liczby już powiedziałeś i jaki jest następny numer. Nie dbam o szczegóły.
    Ten krok odpowiada wywołaniu .next()obiektu generatora.
  • … Powtarzaj poprzedni krok, aż…
  • w końcu możesz skończyć. Nie podajesz mi numeru; krzyczysz: „trzymaj konie! Skończyłem! Żadnych liczb!”
    Ten krok odpowiada obiektowi generatora kończącemu zadanie iStopIteration zgłoszeniu wyjątku Funkcja generatora nie musi zgłaszać wyjątku. Jest podnoszony automatycznie, gdy funkcja kończy się lub wydaje a return.

Tak właśnie działa generator (funkcja zawierająca a yield); zaczyna się wykonywać, zatrzymuje się za każdym razem, gdy wykonuje a yield, a po zapytaniu o .next()wartość kontynuuje od momentu, w którym był ostatni. Idealnie pasuje do projektu z iteracyjnym protokołem Pythona, który opisuje, jak sekwencyjnie żądać wartości.

Najbardziej znanym użytkownikiem protokołu iteratora jest forpolecenie w Pythonie. Tak więc, kiedykolwiek zrobisz:

for item in sequence:

nie ma znaczenia, czy sequencejest to lista, ciąg, słownik lub obiekt generatora, jak opisano powyżej; wynik jest taki sam: odczytujesz elementy z sekwencji jeden po drugim.

Zauważ, że defwprowadzenie funkcji zawierającej yieldsłowo kluczowe nie jest jedynym sposobem na utworzenie generatora; to tylko najprostszy sposób na jego utworzenie.

Aby uzyskać dokładniejsze informacje, przeczytaj o typach iteratorów , instrukcji fedr i generatorach w dokumentacji Pythona.

tzot
źródło
130

Wiele odpowiedzi pokazuje, dlaczego warto użyć yieldgeneratora, ale jest więcej zastosowań yield. Bardzo łatwo jest stworzyć coroutine, która umożliwia przekazywanie informacji między dwoma blokami kodu. Nie powtórzę żadnego z dobrych przykładów, które zostały już podane na temat yieldtworzenia generatora.

Aby zrozumieć, co yieldrobi a w poniższym kodzie, możesz użyć palca, aby prześledzić cykl przez dowolny kod, który ma yield. Za każdym razem, gdy palec dotyka yield, musisz poczekać na wprowadzenie znaku nextlub send. Kiedy a nextjest wywoływane, prześledzisz kod, dopóki nie naciśniesz yield… kod po prawej stronie yieldjest analizowany i zwracany do osoby dzwoniącej… wtedy czekasz. Po nextponownym wywołaniu wykonujesz kolejną pętlę za pomocą kodu. Zauważysz jednak, że w coroutine yieldmożna również używać z send…, który wyśle ​​wartość od osoby wywołującej do funkcji ustępowania. Jeślisend podano a, toyieldodbiera wysłaną wartość i wypluwa ją po lewej stronie… następnie śledzenie kodu postępuje, aż do yieldponownego trafienia (zwracanie wartości na końcu, tak jakby nextzostała wywołana).

Na przykład:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()
Mike McKerns
źródło
Uroczy! Trampolina (w sensie Lisp). Nieczęsto się je widzi!
00prometheus,
129

Istnieje inne yieldzastosowanie i znaczenie (od Python 3.3):

yield from <expr>

Z PEP 380 - Składnia delegowania do subgeneratora :

Proponowana jest składnia generatora w celu przekazania części swoich operacji innemu generatorowi. Pozwala to na wyodrębnienie części kodu zawierającej „dochód” i umieszczenie go w innym generatorze. Ponadto subgenerator może zwracać wartość, a wartość jest udostępniana generatorowi delegującemu.

Nowa składnia otwiera również pewne możliwości optymalizacji, gdy jeden generator ponownie generuje wartości wytworzone przez inny.

Ponadto ten wprowadzi (od Pythona 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

aby uniknąć pomylenia coroutines ze zwykłym generatorem (dziś yieldjest używany w obu).

Sławomir Lenart
źródło
117

Wszystkie świetne odpowiedzi, choć trochę trudne dla początkujących.

Zakładam, że nauczyłeś się tego returnoświadczenia.

Analogicznie, returni yieldsą bliźniakami. returnoznacza „zwróć i zatrzymaj”, podczas gdy „dochód” oznacza „zwróć, ale kontynuuj”

  1. Spróbuj zdobyć num_list z return.
def num_list(n):
    for i in range(n):
        return i

Uruchom:

In [5]: num_list(3)
Out[5]: 0

Widzisz, dostajesz tylko jeden numer, a nie ich listę. returnnigdy nie pozwala ci zwyciężyć, po prostu wdraża raz i wychodzi.

  1. Nadchodzi yield

Wymienić returnz yield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

Teraz wygrywasz, aby uzyskać wszystkie liczby.

W porównaniu z tym, returnktóry uruchamia się raz, a zatrzymuje, yielduruchamia zaplanowane czasy. Możesz interpretować returnjako return one of themi yieldjako return all of them. To się nazywa iterable.

  1. Jeszcze jeden krok, przy pomocy którego możemy przepisać yieldinstrukcjęreturn
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

To jest sedno yield.

Różnica między returndanymi yieldwyjściowymi listy a danymi wynikowymi obiektu jest następująca:

Zawsze otrzymasz [0, 1, 2] z obiektu listy, ale tylko yieldraz możesz je odzyskać z „obiektu wynikowego”. Tak więc ma nowy generatorobiekt nazwy wyświetlany w Out[11]: <generator object num_list at 0x10327c990>.

Podsumowując, jako metafora, by to zrozumieć:

  • returni yieldsą bliźniakami
  • listi generatorsą bliźniakami
Rachunek różniczkowy
źródło
Jest to zrozumiałe, ale jedną z głównych różnic jest to, że można uzyskać wiele wydajności w funkcji / metodzie. W tym momencie analogia całkowicie się załamuje. Yield zapamiętuje swoje miejsce w funkcji, więc przy następnym wywołaniu next () funkcja przechodzi do następnego yield. Myślę, że to ważne i należy to wyrazić.
Mike S
104

Oto kilka przykładów Pythona, jak w rzeczywistości zaimplementować generatory, tak jakby Python nie dostarczył dla nich cukru składniowego:

Jako generator Pythona:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

Używanie zamknięć leksykalnych zamiast generatorów

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

Używanie zamknięć obiektów zamiast generatorów (ponieważ ClosuresAndObjectsAreEquivalent )

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)
Dustin Getz
źródło
97

Zamierzałem zamieścić „przeczytaj stronę 19„ Python: Essential Reference ”Beazleya, aby uzyskać szybki opis generatorów”, ale wiele innych opublikowało już dobre opisy.

Zauważ też, że yieldmożna go używać w koronach jako podwójnego ich zastosowania w funkcjach generatora. Chociaż nie jest to takie samo zastosowanie jak fragment kodu, (yield)może być użyte jako wyrażenie w funkcji. Gdy program wywołujący wysyła wartość do metody przy użyciu send()metody, wówczas coroutine będzie wykonywana do momentu (yield)napotkania następnej instrukcji.

Generatory i korporacje to świetny sposób na konfigurację aplikacji typu przepływ danych. Pomyślałem, że warto wiedzieć o innym zastosowaniu tego yieldwyrażenia w funkcjach.

johnzachary
źródło
97

Z punktu widzenia programowania, iteratory są implementowane jako thunks .

Aby zaimplementować iteratory, generatory i pule wątków w celu równoczesnego wykonywania itp. Jako „thunks” (zwane również funkcjami anonimowymi), wykorzystuje się wiadomości wysłane do obiektu zamknięcia, który ma program rozsyłający, a program rozsyłający odpowiada na „wiadomości”.

http://en.wikipedia.org/wiki/Message_passing

next ” to wiadomość wysłana do zamknięcia, utworzona przez wywołanie „ iter ”.

Istnieje wiele sposobów implementacji tego obliczenia. Użyłem mutacji, ale łatwo jest to zrobić bez mutacji, zwracając aktualną wartość i następnego pomocnika.

Oto demonstracja wykorzystująca strukturę R6RS, ale semantyka jest absolutnie identyczna jak w Pythonie. Jest to ten sam model obliczeń i do przepisania go w Pythonie wymagana jest tylko zmiana składni.

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->
Alinsoar
źródło
84

Oto prosty przykład:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

Wynik:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

Nie jestem programistą Python, ale wydaje mi się, że yieldutrzymuje pozycję przepływu programu, a następna pętla rozpoczyna się od pozycji „fed”. Wygląda na to, że czeka w tej pozycji, a tuż przed tym zwraca wartość na zewnątrz, a następnym razem będzie działać.

To wydaje się być interesującą i miłą umiejętnością: D

Engin OZTURK
źródło
Masz rację. Ale jaki wpływ na przepływ ma zobaczyć zachowanie „plonu”? Mogę zmienić algorytm w imię matematyki. Czy pomoże to uzyskać inną ocenę „wydajności”?
Engin OZTURK,
68

Oto mentalny obraz tego, co yieldrobi.

Lubię myśleć o wątku jako posiadającym stos (nawet jeśli nie jest zaimplementowany w ten sposób).

Kiedy wywoływana jest normalna funkcja, umieszcza zmienne lokalne na stosie, wykonuje pewne obliczenia, a następnie usuwa stos i zwraca. Wartości lokalnych zmiennych nigdy nie są widziane.

Dzięki yieldfunkcji, gdy kod zaczyna działać (tj. Po wywołaniu funkcji, zwracając obiekt generatora, którego next()metoda jest następnie wywoływana), podobnie umieszcza zmienne lokalne na stosie i przez chwilę oblicza. Ale kiedy trafi na yieldinstrukcję, przed wyczyszczeniem części stosu i zwróceniem, pobiera migawkę swoich zmiennych lokalnych i przechowuje je w obiekcie generatora. Zapisuje również miejsce, w którym aktualnie jest ustawiony, w swoim kodzie (tj. Konkretna yieldinstrukcja).

Jest to więc rodzaj zamrożonej funkcji, na której opiera się generator.

Kiedy next()jest wywoływany później, pobiera rzeczy z funkcji na stos i ponownie go animuje. Funkcja kontynuuje obliczanie od momentu jej przerwania, nieświadomy faktu, że właśnie spędził wieczność w chłodni.

Porównaj następujące przykłady:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

Kiedy wywołujemy drugą funkcję, zachowuje się ona zupełnie inaczej niż pierwsza. yieldOświadczenie może być nieosiągalny, ale jeśli jest obecny wszędzie, to zmienia charakter z czym mamy do czynienia.

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

Wywołanie yielderFunction()nie uruchamia kodu, ale generuje generator z kodu. (Być może dobrym pomysłem jest nazywanie takich rzeczy yielderprefiksem dla czytelności).

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

gi_codeI gi_framepola są przechowywane gdzie stanie zamrożonym. Badając je dir(..), możemy potwierdzić, że nasz powyższy model mentalny jest wiarygodny.

Jewgienij Siergiejew
źródło
59

Jak sugeruje każda odpowiedź, yieldsłuży do tworzenia generatora sekwencji. Służy do dynamicznego generowania sekwencji. Na przykład podczas czytania pliku linia po linii w sieci można użyć tej yieldfunkcji w następujący sposób:

def getNextLines():
   while con.isOpen():
       yield con.read()

Możesz użyć go w swoim kodzie w następujący sposób:

for line in getNextLines():
    doSomeThing(line)

Wykonanie Kontrola przeniesienia gotcha

Kontrola wykonania zostanie przeniesiona z getNextLines () do forpętli, gdy wykonywana jest wydajność. Dlatego za każdym razem, gdy wywoływana jest metoda getNextLines (), wykonywanie rozpoczyna się od momentu, w którym zostało ostatnio wstrzymane.

Krótko mówiąc, funkcja z następującym kodem

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

wydrukuje

"first time"
"second time"
"third time"
"Now some useful value 12"
Mangu Singh Rajpurohit
źródło
59

Łatwy przykład, aby zrozumieć, co to jest: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

Dane wyjściowe to:

1 2 1 2 1 2 1 2
Gavriel Cohen
źródło
5
jesteś pewien co do tego wyniku? czy nie byłoby to wydrukowane tylko w jednym wierszu, gdybyś użył tej instrukcji drukowania za pomocą print(i, end=' ')? W przeciwnym razie uważam, że domyślne zachowanie spowodowałoby umieszczenie każdej liczby w nowym wierszu
użytkownik9074332
@ user9074332, Masz rację, ale napisano w jednym wierszu, aby ułatwić zrozumienie
Gavriel Cohen
57

(Moja poniższa odpowiedź mówi tylko z perspektywy korzystania z generatora Python, a nie z podstawowej implementacji mechanizmu generatora , który wymaga pewnych sztuczek w stosie i manipulacji stosem.)

Kiedy yieldjest używana zamiast returnw funkcji python, funkcja ta jest przekształcana w coś specjalnego o nazwie generator function. Ta funkcja zwróci obiekt generatortypu. Słowo yieldkluczowe jest flagą informującą kompilator Pythona, aby specjalnie traktował taką funkcję. Normalne funkcje zakończą się, gdy jakaś wartość zostanie z niej zwrócona. Ale za pomocą kompilatora można uznać, że funkcja generatora może zostać wznowiona. Oznacza to, że kontekst wykonania zostanie przywrócony, a wykonanie będzie kontynuowane od ostatniego uruchomienia. Aż do jawnego wywołania return, który zgłosi StopIterationwyjątek (który jest również częścią protokołu iteratora) lub dojdzie do końca funkcji. Znalazłem wiele referencji na generatorten temat jedenz functional programming perspectivenajbardziej strawnego.

(Teraz chcę porozmawiać o uzasadnieniu generatori iteratormoim własnym zrozumieniu. Mam nadzieję, że pomoże ci to zrozumieć istotną motywację iteratora i generatora. Taka koncepcja pojawia się również w innych językach, takich jak C #).

Jak rozumiem, kiedy chcemy przetworzyć wiązkę danych, zwykle najpierw przechowujemy dane gdzieś, a następnie przetwarzamy je jeden po drugim. Ale to naiwne podejście jest problematyczne. Jeśli ilość danych jest ogromna, wcześniejsze przechowywanie ich jako całości jest drogie. Zamiast więc przechowywać dane databezpośrednio, dlaczego nie przechowywać jakiegoś rodzaju metadatapośrednio, tjthe logic how the data is computed .

Istnieją 2 podejścia do zawijania takich metadanych.

  1. Podejście OO, zawijamy metadane as a class. Jest to tak zwany, iteratorktóry implementuje protokół iteratora (tj. Metody __next__()i __iter__()). Jest to również często spotykany wzorzec projektowy iteratora .
  2. Podejście funkcjonalne, opakowujemy metadane as a function. To jest tak zwane generator function. Ale pod maską zwrócony generator objectwciąż IS-Aiterator, ponieważ implementuje również protokół iteratora.

Tak czy inaczej, tworzony jest iterator, tzn. Jakiś obiekt, który może dać ci dane, których potrzebujesz. Podejście OO może być nieco skomplikowane. W każdym razie, który z nich należy do ciebie, zależy od ciebie.

smwikipedia
źródło
54

Podsumowując, yieldinstrukcja przekształca twoją funkcję w fabrykę, która wytwarza specjalny obiekt o nazwie generatorowinięty wokół ciała twojej oryginalnej funkcji. Kiedy generatoriteruje się, wykonuje twoją funkcję, dopóki nie osiągnie następnego, yielda następnie zawiesza wykonanie i ocenia na przekazaną wartość yield. Powtarza ten proces przy każdej iteracji, dopóki ścieżka wykonania nie opuści funkcji. Na przykład,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

po prostu wyjścia

one
two
three

Moc pochodzi z zastosowania generatora z pętlą, która oblicza sekwencję, generator wykonuje pętlę zatrzymując się za każdym razem, aby „wydać” kolejny wynik obliczeń, w ten sposób oblicza listę w locie, korzyścią jest pamięć zapisane dla szczególnie dużych obliczeń

Załóżmy, że chcesz utworzyć własną rangefunkcję, która generuje iterowalny zakres liczb, możesz to zrobić w ten sposób,

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

i użyj tego w ten sposób;

for i in myRangeNaive(10):
    print i

Ale to jest nieefektywne, ponieważ

  • Tworzysz tablicę, której używasz tylko raz (marnuje to pamięć)
  • Ten kod faktycznie zapętla się dwukrotnie w tej tablicy! :(

Na szczęście Guido i jego zespół byli wystarczająco hojni, aby opracować generatory, abyśmy mogli to zrobić;

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

Teraz przy każdej iteracji funkcja na wywoływanym generatorze next()wykonuje funkcję, dopóki nie osiągnie instrukcji „wydajność”, w której zatrzymuje się i „daje” wartość lub osiąga koniec funkcji. W tym przypadku przy pierwszym wywołaniu next()wykonuje się do instrukcji fed i wydaj 'n', przy następnym wywołaniu wykona instrukcję przyrostu, wróci do „while”, oceni ją, a jeśli to prawda, zatrzyma się i ponownie wydaj 'n', będzie to kontynuowało do momentu, aż warunek while zwróci wartość false, a generator przejdzie do końca funkcji.

redbandit
źródło
53

Wydajność jest przedmiotem

A returnw funkcji zwróci pojedynczą wartość.

Jeśli chcesz, aby funkcja zwróciła ogromny zestaw wartości , użyj yield.

Co ważniejsze, yieldstanowi barierę .

jak bariera w języku CUDA, nie przekaże kontroli, dopóki nie zostanie ukończona.

Oznacza to, że uruchomi kod w twojej funkcji od początku, aż do momentu trafienia yield. Następnie zwróci pierwszą wartość pętli.

Następnie każde kolejne wywołanie uruchomi pętlę, którą napisałeś w funkcji jeszcze raz, zwracając następną wartość, dopóki nie będzie żadnej wartości do zwrócenia.

Kaleem Ullah
źródło
52

Wiele osób używa returnzamiast yield, ale w niektórych przypadkach yieldmoże być bardziej efektywne i łatwiejsze w użyciu.

Oto przykład, który yieldjest zdecydowanie najlepszy dla:

return (w funkcji)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

wydajność (w funkcji)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

Funkcje wywoływania

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

Obie funkcje robią to samo, ale yieldużywają trzech linii zamiast pięciu i mają o jedną zmienną mniej do zmartwienia.

Jest to wynik kodu:

Wynik

Jak widać, obie funkcje robią to samo. Jedyna różnica polega na return_dates()podaniu listy i yield_dates()generatora.

Przykładem może być czytanie pliku linia po linii lub jeśli chcesz po prostu stworzyć generator.

Tom Fuller
źródło
43

yieldjest jak element zwrotny dla funkcji. Różnica polega na tym, że yieldelement zamienia funkcję w generator. Generator zachowuje się jak funkcja, dopóki coś nie zostanie „wydane”. Generator zatrzymuje się, dopóki nie zostanie ponownie wywołany, i kontynuuje od dokładnie tego samego punktu, w którym się uruchomił. Możesz uzyskać sekwencję wszystkich „uzyskanych” wartości w jednym, wywołując list(generator()).

Will Dereham
źródło
41

Słowo yieldkluczowe po prostu zbiera zwracane wyniki. Myśleć o yieldniczymreturn +=

Bahtiyar Özdere
źródło
36

Oto proste yieldpodejście do obliczenia serii Fibonacciego, wyjaśnione:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

Gdy wpiszesz to do REPL, a następnie spróbujesz go nazwać, otrzymasz tajemniczy rezultat:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

Jest tak, ponieważ obecność yieldsygnalizowanego Pythonowi, że chcesz utworzyć generator , to znaczy obiekt, który generuje wartości na żądanie.

Jak więc generować te wartości? Można to zrobić albo bezpośrednio, używając wbudowanej funkcji next, albo pośrednio, przekazując ją do konstrukcji, która zużywa wartości.

Korzystając z wbudowanej next()funkcji, bezpośrednio wywołujesz .next/ __next__, zmuszając generator do wygenerowania wartości:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

Pośrednio, jeśli dostarczysz fibdo forpętli, listinicjalizatora, tupleinicjatora lub czegokolwiek innego, co oczekuje, że obiekt generuje / generuje wartości, „zużyjesz” generator, dopóki nie będzie mógł wygenerować więcej wartości (i zwróci) :

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

Podobnie z tupleinicjatorem:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

Generator różni się od funkcji tym, że jest leniwy. Dokonuje tego, utrzymując swój stan lokalny i umożliwiając wznowienie, kiedy tylko zajdzie taka potrzeba.

Gdy wywołujesz fibgo po raz pierwszy :

f = fib()

Python kompiluje funkcję, napotyka yieldsłowo kluczowe i po prostu zwraca obiekt generatora. Nie wydaje się to zbyt pomocne.

Kiedy następnie zażądasz, generuje pierwszą wartość, bezpośrednio lub pośrednio, wykonuje wszystkie znalezione instrukcje, dopóki nie napotka znaku a yield, a następnie zwraca wartość, którą podałeś yieldi zatrzymuje się. Na przykład, który lepiej to pokazuje, użyjmy niektórych printwywołań (zamień print "text"na if w Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

Teraz wprowadź REPL:

>>> gen = yielder("Hello, yield!")

masz teraz obiekt generatora, który czeka na polecenie wygenerowania wartości. Użyj nexti zobacz, co zostanie wydrukowane:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Niecytowane wyniki są wydrukowane. Podany wynik jest zwracany yield. Zadzwoń nextponownie teraz:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Generator pamięta, że ​​został zatrzymany yield valuei wznawia stamtąd. Następna wiadomość jest drukowana i wyszukiwanie yieldinstrukcji, która ma zostać wstrzymana, jest wykonywane ponownie (z powodu whilepętli).

Dimitris Fasarakis Hilliard
źródło