W praktyce, jakie są główne zastosowania nowej składni "yield from" w Pythonie 3.3?

Mam problem z owijaniem mojego mózgu wokół PEP 380 .

  1. Jakie są sytuacje, w których "poddanie się" jest przydatne?
  2. jaki jest klasyczny przypadek użycia?
  3. Dlaczego jest porównywany do mikro-wątków?

[update]

Teraz rozumiem przyczynę moich trudności. Używałem generatorów, ale nigdy tak naprawdę nie używałem korutin (wprowadzonych przez PEP-342 ). Pomimo pewnych podobieństw, generatory i koroutiny są zasadniczo dwoma różnymi koncepcje. Zrozumienie koroutinów (nie tylko generatorów) jest kluczem do zrozumienia nowej składni.

IMHO coroutines to najbardziej niejasna funkcja 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 komentarz linkujący do David Beazley prezentacje. David rządzi.

 243
Author: Community, 2012-03-14

6 answers

Zacznijmy od jednej rzeczy. Wyjaśnienie, że yield from g jest równoważne for v in g: yield v nawet nie zaczyna wymierzać sprawiedliwości temu, o co w tym wszystkim chodzi. Ponieważ, spójrzmy prawdzie w oczy, jeśli wszystko {[13] } rozszerza pętlę for, to nie gwarantuje to dodania yield from do języka i uniemożliwia implementację całej gamy nowych funkcji w Pythonie 2.x.

Co yield from robi to ustanawia przejrzyste dwukierunkowe połączenie między rozmówca i sub-generator:

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

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

(jeśli mówimy o TCP, yield from g może oznaczać " teraz tymczasowo odłącz mój Gniazdo klienta i podłącz je ponownie do tego innego gniazda serwera".)

BTW, jeśli nie jesteś pewien co wysyłanie danych do generatora oznacza nawet, musisz rzucić wszystko i poczytać o koroutinach-są one bardzo przydatne (kontrast z podprogramami), ale niestety mniej znane w Pythonie. Ciekawy kurs Dave ' a Beazleya na Couroutines to doskonały początek. przeczytaj slajdy 24-33 dla szybkiego podkładu.

Czytanie dane z generatora wykorzystującego wydajność 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 po prostu yield from to.

def reader_wrapper(g):
    yield from g
To działa i wyeliminowaliśmy jedną linijkę kodu. I prawdopodobnie intencja jest nieco jaśniejsza (lub nie). Ale nic się nie zmienia. [[78]}wysyłanie danych do generatora (coroutine) przy użyciu wydajności z-Część 1 Teraz zróbmy coś ciekawszego. Stwórzmy koroutine o nazwie writer, która przyjmuje dane wysłane do niego i zapisuje do socket, 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, jak funkcja wrapper radzi sobie z wysyłaniem danych do Writera, aby wszelkie dane, które są wysyłane do wrappera byłytransparentnie wysył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

Wrapper musi akceptować dane, które są do niego wysyłane (oczywiście) i powinien obsługiwać StopIteration gdy pętla for jest wyczerpana. Najwyraźniej samo robienie for x in coro: yield x nie wystarczy. 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 zrobić to.

def writer_wrapper(coro):
    yield from coro

To oszczędza 6 linijek kodu, czyni go o wiele bardziej czytelnym i po prostu działa. Magia!

Wysyłanie danych do generatora wydajności z-Część 2-Obsługa wyjątków

Skomplikujmy to. Co jeśli nasz pisarz musi zająć się wyjątkami? Załóżmy, że writer obsługuje SpamException i drukuje ***, jeśli go napotka.
class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)
A jeśli się nie zmienimy? 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

Um, to nie działa, ponieważ x = (yield) tylko podnosi wyjątek i wszystko się zatrzyma. Niech to zadziała, ale ręczne obchodzenie się z wyjątkami i wysyłanie ich lub wrzucanie do sub-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 to też!
def writer_wrapper(coro):
    yield from coro

yield from transparentnie obsługuje wysyłanie wartości lub wrzucanie wartości do sub-generatora.

To nadal nie obejmuje wszystkich narożników. Co się stanie, jeśli zewnętrzny generator jest zamknięty? A co z w przypadku, gdy sub-generator Zwraca wartość (tak, w Pythonie 3.3+ generatory mogą zwracać wartości), w jaki sposób należy propagować wartość zwracaną? toyield from przezroczyste uchwyty wszystkich narożników jest naprawdę imponujące . Po prostu magicznie działa i zajmuje się tymi wszystkimi sprawami.

Osobiście uważam, że yield from jest słabym wyborem słów kluczowych, ponieważ nie sprawia, żedwukierunkowa natura jest widoczna. Zaproponowano inne słowa kluczowe (jak delegate, ale zostały odrzucone, ponieważ dodanie nowego słowa kluczowego do języka jest znacznie trudniejsze niż łączenie istniejących.

Podsumowując, najlepiej myśleć yield from jako transparent two way channel pomiędzy rozmówcą a sub-generatorem.

Bibliografia:

  1. PEP 380 - składnia dla sub-generatora (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Coroutines via Enhanced Generators (GvR, Eby) [v2.5, 2005-05-10]
 341
Author: Praveen Gollakota,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-01 00:00:19

Jakie są sytuacje, w których "poddanie się" jest przydatne?

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

for x in subgenerator:
  yield x

Jak opisuje PEP, jest to raczej naiwna próba użycia subgeneratora, brakuje w nim kilku aspektów, zwłaszcza właściwego obchodzenia się z .throw()/.send()/.close() mechanizmy wprowadzone przez PEP 342 . Aby to zrobić poprawnie, dość skomplikowany kod jest niezbędny.

Jakie jest Klasyczne użycie case?

Rozważ, że chcesz wyodrębnić informacje z rekurencyjnej struktury danych. Załóżmy, ż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 czasu yield from nie było prostej metody refaktoryzacji kodu generatora. Załóżmy, że masz (bezsensowny) generator 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ę uwzględnić te pętle w oddzielnych generatorach. Bez yield from, to jest brzydkie, aż do momentu, w którym pomyślisz dwa razy, czy naprawdę chcesz to zrobić. Z yield from, to faktycznie miło popatrzeć:

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 jest porównywany do mikro-wątków?

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

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

Ta analogia nie jest to jednak nic konkretnego dla yield from - jest to raczej ogólna właściwość generatorów w Pythonie.

 72
Author: Niklas B.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-04 10:31:13

Gdziekolwiek wywołasz generator z generatora, potrzebujesz "pompy" do ponownegoyield wartości: for v in inner_generator: yield v. Jak wskazuje PEP, są w tym subtelne zawiłości, które większość ludzi ignoruje. Nielokalna kontrola przepływu, taka jak throw(), jest jednym z przykładów podanych w PEP. Nowa składnia yield from inner_generator jest używana wszędzie tam, gdzie wcześniej napisałbyś jawną pętlę for. Nie jest to jednak tylko cukier składniowy: obsługuje wszystkie przypadki narożników, które są ignorowane przez pętlę for. Bycie " słodkim" zachęca ludzi do korzystania z niego, a tym samym uzyskać właściwe zachowania.

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

Z dodatkowymi funkcjami generatora wprowadzonymi przez PEP 342, czyli nie dłuższa sprawa: jak opisano w PEPIE Grega, prosta iteracja nie wsparcie send() I throw () poprawnie. Gimnastyka potrzebna do wspomagania send () I throw() w rzeczywistości nie są takie skomplikowane, gdy je łamiesz dół, ale nie są trywialne ani jedno, ani drugie.

Nie mogę mówić o porównaniu Z mikro-nitkami, poza obserwacją, że generatory są rodzajem paralelizmu. Generator zawieszony można uznać za wątek, który wysyła wartości za pośrednictwem yield do wątku konsumenckiego. Rzeczywista implementacja może nie być podobna do tej (A rzeczywista implementacja jest oczywiście bardzo interesująca dla programistów Pythona), ale nie dotyczy to Użytkowników.

Nowa składnia yield from nie dodaje żadnych dodatkowych możliwości języka w zakresie gwintowania, po prostu ułatwia prawidłowe korzystanie z istniejących funkcji. A dokładniej ułatwia początkującemu konsumentowi złożonego wewnętrznego generatora napisanego przez eksperta {20]}przejście przez ten generator bez łamania jego złożonych cech.

 27
Author: Ben Jackson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-14 19:58:51

Krótki przykład pomoże Ci zrozumieć jeden z przypadków użycia yield from: get value from another generator

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]]])))
 13
Author: ospider,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-19 04:58:32

yield from w zasadzie Iteratory łańcuchów w efektywny sposób:

# 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 łączenie iteratorów jest dość powszechnym wzorcem w Pythonie.

Wątki są w zasadzie funkcją, która pozwala wyskakiwać z funkcji w całkowicie losowych punktach i przeskakiwać z powrotem do stanu innej funkcji. Nadzorca wątku robi to bardzo często, więc program wydaje się uruchamiać wszystkie te funkcje w tym samym czasie. Na problem polega na tym, że punkty są losowe, więc musisz użyć blokady, aby zapobiec zatrzymaniu funkcji przez przełożonego w problematycznym punkcie.

Generatory są dość podobne do wątków w tym sensie: pozwalają określić konkretne punkty (kiedy tylko yield), w których można wskoczyć i wyjść. W ten sposób Generatory nazywane są koroutinami.

Przeczytaj ten doskonały samouczek o coroutinach w Pythonie, aby uzyskać więcej szczegółów

 3
Author: Jochen Ritzel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-14 20:02:48

In applied usage for asynchroniczny Io coroutine, yield from ma podobne zachowanie jak await w funkcji koroutine . Oba z nich są używane do zawieszenia wykonania coroutine.

Dla Asyncio, jeśli nie ma potrzeby obsługi starszej wersji Pythona (np. >3.5), async def/await jest zalecaną składnią do definiowania koroutine. Tak więc yield from nie jest już potrzebny w koroutinie.

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

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