W praktyce, jakie są główne zastosowania nowej składni „zysk z” w Pythonie 3.3?

407

Ciężko mi się owija mózg wokół PEP 380 .

  1. Jakie są sytuacje, w których przydatne jest „uzyskiwanie z”?
  2. Jaki jest klasyczny przypadek użycia?
  3. Dlaczego porównuje się go z mikrowątkami?

[ aktualizacja ]

Teraz rozumiem przyczynę moich trudności. Używałem generatorów, ale tak naprawdę nigdy nie używałem koronek (wprowadzonych przez PEP-342 ). Pomimo pewnych podobieństw, generatory i korporacje to w zasadzie dwie różne koncepcje. Zrozumienie coroutines (nie tylko generatorów) jest kluczem do zrozumienia nowej składni.

Korpusy IMHO są najbardziej niejasną funkcją Pythona , większość książek sprawia, że ​​wygląda bezużytecznie i nieciekawie.

Dzięki za świetne odpowiedzi, ale specjalne podziękowania dla agf i jego komentarza do prezentacji Davida Beazleya . David kołysze.

Paulo Scardine
źródło
7
Film z prezentacji dabeaz.com/coroutines Davida Beazleya : youtube.com/watch?v=Z_OAlIhXziw
jcugat

Odpowiedzi:

570

Najpierw usuńmy jedno. Wyjaśnienie, które yield from gjest równoważne z for v in g: yield v tym, nie zaczyna nawet oddawać sprawiedliwości temu, o co w tym yield fromwszystkim chodzi. Ponieważ, spójrzmy prawdzie w oczy, jeśli wszystko yield fromzrobi, to rozwinięcie forpętli, nie gwarantuje to dodania yield fromdo języka i wyklucza wprowadzenie całej gamy nowych funkcji w Pythonie 2.x.

Co yield fromto jest ustanawia przejrzyste dwukierunkowe połączenie między dzwoniącym a pod-generatorem :

  • Połączenie jest „przezroczyste” w tym sensie, że propaguje również wszystko poprawnie, nie tylko generowane elementy (np. Propagowane są wyjątki).

  • Połączenie jest „dwukierunkowe” w tym sensie, że dane mogą być wysyłane zarówno z generatora, jak i do niego.

( Gdybyśmy mówili o TCP, yield from gmoże to oznaczać „teraz tymczasowo odłącz gniazdo mojego klienta i podłącz je ponownie do tego drugiego gniazda serwera” ).

BTW, jeśli nie jesteś pewien, co w ogóle oznacza wysyłanie danych do generatora , musisz porzucić wszystko i najpierw przeczytać o coroutines - są one bardzo przydatne (w przeciwieństwie do podprogramów ), ale niestety mniej znane w Pythonie. Ciekawy kurs Dave'a Beazleya na coroutines to doskonały początek. Przeczytaj slajdy 24-33, aby uzyskać szybki podkład.

Odczyt danych z generatora przy użyciu wydajności z

def reader():
    """A generator that fakes a read from a file, socket, etc."""
    for i in range(4):
        yield '<< %s' % i

def reader_wrapper(g):
    # Manually iterate over data produced by reader
    for v in g:
        yield v

wrap = reader_wrapper(reader())
for i in wrap:
    print(i)

# Result
<< 0
<< 1
<< 2
<< 3

Zamiast ręcznie iterować reader(), możemy to yield fromzrobić.

def reader_wrapper(g):
    yield from g

To działa i wyeliminowaliśmy jeden wiersz kodu. I prawdopodobnie cel jest nieco jaśniejszy (lub nie). Ale nic nie zmienia życia.

Przesyłanie danych do generatora (coroutine) z wykorzystaniem wydajności z - Część 1

Zróbmy teraz coś ciekawszego. Stwórzmy nazwaną coroutine, writerktóra akceptuje wysyłane do niej dane i zapisuje w gnieździe, fd itp.

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

Teraz pytanie brzmi: w jaki sposób funkcja opakowania powinna obsługiwać wysyłanie danych do programu piszącego, aby wszelkie dane wysyłane do opakowania były w sposób przezroczysty przesyłane do writer()?

def writer_wrapper(coro):
    # TBD
    pass

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in range(4):
    wrap.send(i)

# Expected result
>>  0
>>  1
>>  2
>>  3

Opakowanie musi zaakceptować dane, które są do niego wysyłane (oczywiście) i powinno również obsłużyć StopIterationmoment wyczerpania pętli for. Najwyraźniej samo robienie for x in coro: yield xtego nie da. Oto wersja, która działa.

def writer_wrapper(coro):
    coro.send(None)  # prime the coro
    while True:
        try:
            x = (yield)  # Capture the value that's sent
            coro.send(x)  # and pass it to the writer
        except StopIteration:
            pass

Lub możemy to zrobić.

def writer_wrapper(coro):
    yield from coro

Oszczędza to 6 linii kodu, czyni go znacznie bardziej czytelnym i po prostu działa. Magia!

Przesyłanie danych do generatora generuje z - Część 2 - Obsługa wyjątków

Zróbmy to bardziej skomplikowanym. Co jeśli nasz pisarz musi poradzić sobie z wyjątkami? Powiedzmy, że writeruchwyty SpamExceptiona drukuje, ***jeśli je napotka.

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

Co jeśli się nie zmienimy writer_wrapper? Czy to działa? Spróbujmy

# writer_wrapper same as above

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
    if i == 'spam':
        wrap.throw(SpamException)
    else:
        wrap.send(i)

# Expected Result
>>  0
>>  1
>>  2
***
>>  4

# Actual Result
>>  0
>>  1
>>  2
Traceback (most recent call last):
  ... redacted ...
  File ... in writer_wrapper
    x = (yield)
__main__.SpamException

Nie działa, bo x = (yield)podnosi wyjątek i wszystko się kończy. Sprawmy, by działało, ale ręcznie obsługujemy wyjątki i wysyłamy je lub wrzucamy do pod-generatora ( writer)

def writer_wrapper(coro):
    """Works. Manually catches exceptions and throws them"""
    coro.send(None)  # prime the coro
    while True:
        try:
            try:
                x = (yield)
            except Exception as e:   # This catches the SpamException
                coro.throw(e)
            else:
                coro.send(x)
        except StopIteration:
            pass

To działa.

# Result
>>  0
>>  1
>>  2
***
>>  4

Ale tak też się dzieje!

def writer_wrapper(coro):
    yield from coro

W yield fromprzejrzysty uchwyty wysyłania wartości lub rzucanie wartości do sub-generator.

Nie dotyczy to jednak wszystkich przypadków narożnych. Co się stanie, jeśli zewnętrzny generator zostanie zamknięty? Co z przypadkiem, gdy pod-generator zwraca wartość (tak, w Pythonie 3.3+, generatory mogą zwracać wartości), w jaki sposób należy propagować zwracaną wartość? To, że w yield fromprzejrzysty sposób obsługuje wszystkie skrzynki narożne, jest naprawdę imponujące . yield frompo prostu magicznie działa i obsługuje wszystkie te przypadki.

Osobiście uważam, że yield fromjest to zły wybór słów kluczowych, ponieważ nie uwidacznia to dwukierunkowej natury. Zaproponowano inne słowa kluczowe (jak, delegateale zostały odrzucone, ponieważ dodanie nowego słowa kluczowego do języka jest znacznie trudniejsze niż połączenie istniejących.

Podsumowując, najlepiej jest traktować to yield fromjako połączenie transparent two way channelmiędzy dzwoniącym a sub-generatorem.

Bibliografia:

  1. PEP 380 - Składnia delegowania do pod-generatora (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Coroutines via Enhanced Generators (GvR, Eby) [v2.5, 2005-05-10]
Praveen Gollakota
źródło
3
@PraveenGollakota, w drugiej części twojego pytania, Wysyłanie danych do generatora (coroutine) z wykorzystaniem dochodu z - Część 1 , a jeśli masz więcej niż coroutines do przekazania otrzymanego przedmiotu? Podobnie jak w przypadku scenariusza nadawcy lub subskrybenta, w którym podajesz wiele reklam do opakowania w swoim przykładzie, a elementy powinny być wysyłane do wszystkich lub podzbioru?
Kevin Ghaboosi
3
@PraveenGollakota, Kudos za świetną odpowiedź. Małe przykłady pozwalają mi wypróbować rzeczy w repl. Link do kursu Dave'a Beazleya był bonusem!
BiGYaN
1
wykonanie except StopIteration: passWEWNĘTRZNEJ while True:pętli nie jest dokładnym odwzorowaniem yield from coro- która nie jest nieskończoną pętlą i po corowyczerpaniu (tj. podnosi StopIteration), writer_wrapperwykona następną instrukcję. Po ostatnim StopIteration
zdaniu sam się podbije
1
... więc jeśli zostanie writerzawarty for _ in range(4)zamiast while True, to po wydrukowaniu >> 3TAKŻE również automatycznie podniesie, StopIterationa to będzie automatycznie obsługiwane przez, yield froma następnie writer_wrapperautomatycznie podniesie swoje własne, StopIterationa ponieważ wrap.send(i)nie jest w trybloku, w rzeczywistości zostanie podniesione w tym momencie ( tzn. traceback zgłasza tylko linię wrap.send(i), a nie nic z wnętrza generatora)
Aprillion
3
Po przeczytaniu „ nawet nie zaczyna czynić sprawiedliwości ”, wiem, że doszedłem do właściwej odpowiedzi. Dziękuję za wspaniałe wyjaśnienie!
Hot.PxL
89

Jakie są sytuacje, w których przydatne jest „uzyskiwanie z”?

Każda sytuacja, w której masz taką pętlę:

for x in subgenerator:
  yield x

Jak opisuje PEP, jest to dość naiwna próba użycia subgeneratora, brakuje w niej kilku aspektów, zwłaszcza poprawnej obsługi mechanizmów .throw()/ .send()/ .close()wprowadzonych przez PEP 342 . Aby zrobić to poprawnie, potrzebny jest raczej skomplikowany kod.

Jaki jest klasyczny przypadek użycia?

Rozważ, że chcesz wyodrębnić informacje ze struktury danych rekurencyjnych. Powiedzmy, że chcemy uzyskać wszystkie węzły liści w drzewie:

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)

Jeszcze ważniejsze jest to, że do tego czasu yield fromnie było prostej metody refaktoryzacji kodu generatora. Załóżmy, że masz (bezsensowny) generator taki jak ten:

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)

Teraz decydujesz się rozdzielić te pętle na osobne generatory. Bez yield fromtego jest to brzydkie, do tego stopnia, że ​​pomyślisz dwa razy, czy naprawdę chcesz to zrobić. Dzięki yield fromnaprawdę miło jest spojrzeć na:

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)

Dlaczego porównuje się go z mikrowątkami?

Myślę, że w tym rozdziale PEP jest mowa o tym, że każdy generator ma swój własny izolowany kontekst wykonania. Wraz z faktem, że wykonywanie jest przełączane między iteratorem generatora a wywołującym za pomocąyield i __next__(), odpowiednio, jest to podobne do wątków, w których system operacyjny od czasu do czasu przełącza wykonujący wątek, wraz z kontekstem wykonania (stos, rejestry, ...)

Efekt tego jest również porównywalny: zarówno generator-iterator, jak i program wywołujący postępują w stanie wykonania w tym samym czasie, ich wykonania są przeplatane. Na przykład, jeśli generator wykonuje jakieś obliczenia, a program wywołujący wydrukuje wyniki, zobaczysz wyniki, gdy tylko będą dostępne. Jest to forma współbieżności.

Ta analogia nie jest jednak niczym szczególnym yield from- jest raczej ogólną właściwością generatorów w Pythonie.

Niklas B.
źródło
Refaktoryzacja generatorów jest dziś bolesna .
Josh Lee
1
Często używam itertools do refaktoryzacji generatorów (rzeczy takie jak itertools.chain), to nie jest wielka sprawa. Lubię czerpać zyski, ale wciąż nie widzę, jak rewolucyjny jest. Prawdopodobnie jest tak, ponieważ Guido jest szalony z tego powodu, ale muszę przegapić duży obraz. Wydaje mi się, że świetnie nadaje się do send (), ponieważ trudno jest to refaktoryzować, ale nie używam tego dość często.
e-satis
Sądzę, że get_list_values_as_xxxsą to proste generatory z jedną linią for x in input_param: yield int(x)i dwoma pozostałymi odpowiednio z strifloat
madtyn
@NiklasB. re „wyodrębnij informacje ze struktury danych rekurencyjnych”. Właśnie dostaję się do Py po dane. Czy możesz zrobić dźgnięcie w tym Q ?
alancalvitti
33

Gdziekolwiek wywołać generatora od wewnątrz generatora trzeba „pompa”, aby ponownie yieldwartości: for v in inner_generator: yield v. Jak wskazuje PEP, istnieją subtelne zawiłości, które większość ludzi ignoruje. Nielokalna kontrola przepływu, podobnie jak throw()jeden przykład podany w PEP. Nowa składnia yield from inner_generatorjest używana wszędzie tam, gdzie wcześniej napisałeś wyraźną forpętlę. Nie jest to jednak tylko cukier składniowy: obsługuje wszystkie narożne przypadki, które są ignorowane przez forpętlę. Bycie „słodkim” zachęca ludzi do korzystania z niego, a tym samym do uzyskania właściwych zachowań.

Ta wiadomość w wątku dyskusyjnym mówi o tych złożonościach:

Dzięki dodatkowym funkcjom generatora wprowadzonym przez PEP 342 już tak nie jest: jak opisano w PEP Grega, prosta iteracja nie obsługuje poprawnie send () i throw (). Gimnastyka potrzebna do obsługi send () i throw () w rzeczywistości nie jest tak skomplikowana, gdy się je rozbije, ale też nie są trywialne.

Nie mogę porozmawiać o porównaniu z mikrowątkami poza obserwacją, że generatory są rodzajem paralelizmu. Możesz uznać zawieszony generator za wątek, który przesyła wartości yielddo wątku klienta. Rzeczywista implementacja może być podobna do tej (a rzeczywista implementacja jest oczywiście bardzo interesująca dla programistów Python), ale nie dotyczy to użytkowników.

Nowa yield fromskładnia nie dodaje do języka żadnych dodatkowych możliwości w zakresie wątków, po prostu ułatwia prawidłowe korzystanie z istniejących funkcji. Mówiąc dokładniej, nowicjuszowi złożonemu generatorowi wewnętrznemu napisanemu przez eksperta łatwiej jest przejść przez ten generator bez naruszania jego złożonych funkcji.

Ben Jackson
źródło
23

Krótki przykład pomoże ci zrozumieć jeden z yield fromprzypadków użycia: uzyskaj wartość z innego generatora

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))
ospider
źródło
2
Chciałem tylko zasugerować, że wydruk na końcu wyglądałby trochę ładniej bez konwersji na listę -print(*flatten([1, [2], [3, [4]]]))
yoniLavi
6

yield from w zasadzie efektywnie łączy iteratory:

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it

Jak widać, usuwa jedną czystą pętlę Pythona. To prawie wszystko, co robi, ale tworzenie łańcuchów iteratorów jest dość powszechnym wzorcem w Pythonie.

Wątki są w zasadzie funkcją, która pozwala wyskoczyć z funkcji w całkowicie losowych punktach i wrócić do stanu innej funkcji. Nadzorca wątku robi to bardzo często, więc program wydaje się uruchamiać wszystkie te funkcje jednocześnie. Problem polega na tym, że punkty są losowe, więc musisz użyć blokowania, aby zapobiec zatrzymaniu funkcji przez przełożonego w problematycznym punkcie.

Generatory są bardzo podobne do wątków w tym sensie: pozwalają określić określone punkty (za każdym razem, gdy yield), w których można wskakiwać i wyskakiwać. Gdy są używane w ten sposób, generatory nazywane są coroutines.

Przeczytaj te doskonałe samouczki na temat coroutines w Pythonie, aby uzyskać więcej informacji

Jochen Ritzel
źródło
10
Ta odpowiedź jest myląca, ponieważ pomija istotną cechę „fed z”, jak wspomniano powyżej: obsługa send () i throw ().
Justin W
2
@Justin W: Wydaje mi się, że wszystko, co przeczytałeś wcześniej, jest w rzeczywistości mylące, ponieważ nie zrozumiałeś, że throw()/send()/close()są to yieldfunkcje, które yield fromoczywiście muszą zostać poprawnie zaimplementowane, ponieważ mają uprościć kod. Takie trywialności nie mają nic wspólnego z użytkowaniem.
Jochen Ritzel
5
Czy kwestionujesz powyższą odpowiedź Bena Jacksona? Moją odpowiedzią na to pytanie jest to, że jest to cukier składniowy, który jest zgodny z podaną przez ciebie transformacją kodu. Odpowiedź Bena Jacksona w szczególności obala to twierdzenie.
Justin W
@JochenRitzel Nigdy nie musisz pisać własnej chainfunkcji, ponieważ itertools.chainjuż istnieje. Zastosowanie yield from itertools.chain(*iters).
Acumenus
3

W zastosowaniu do asynchronicznej IO coroutine , yield fromma podobne zachowanie jak awaitw funkcji coroutine . Oba są wykorzystywane do zawieszenia wykonywania koroutyny.

W przypadku Asyncio, jeśli nie ma potrzeby obsługi starszej wersji języka Python (tj.> 3.5), async def/ awaitjest zalecaną składnią do zdefiniowania coroutine. W yield fromten sposób nie jest już potrzebny w rogówce.

Ale ogólnie poza asyncio, yield from <sub-generator>ma jeszcze inne zastosowanie w iteracji pod-generatora, jak wspomniano we wcześniejszej odpowiedzi.

Yeo
źródło
1

Ten kod definiuje funkcję fixed_sum_digitszwracającą generator zliczający wszystkie sześć cyfr, tak że suma cyfr wynosi 20.

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

Spróbuj napisać bez yield from. Jeśli znajdziesz skuteczny sposób, aby to zrobić, daj mi znać.

Myślę, że w przypadkach takich jak ten: odwiedzanie drzew yield fromczyni kod prostszym i czystszym.

jimifiki
źródło
0

Mówiąc prosto, yield fromzapewnia rekurencję ogona dla funkcji iteratora.

DomQ
źródło