На практиці, які основні напрямки використання нового синтаксису "вихід із" в Python 3.3?


407

Мені важко обернути мозок навколо PEP 380 .

  1. У яких ситуаціях корисний "вихід від"?
  2. Який класичний випадок використання?
  3. Чому його порівняно з мікропотоками?

[оновлення]

Тепер я розумію причину своїх труднощів. Я використовував генератори, але ніколи насправді не використовував корутин (представлений PEP-342 ). Незважаючи на деяку схожість, генератори та супроводи - це в основному дві різні концепції. Розуміння взаємоконтролів (не лише генераторів) - запорука розуміння нового синтаксису.

Спроби IMHO - це найпомітніша особливість Python ; більшість книг роблять це марним і нецікавим.

Дякую за чудові відповіді, але особливу подяку AGF та його коментар, що посилаються на презентації Девіда Бізлі . Девід скелі.



7
Відео з презентації dabeaz.com/coroutines Девіда Бізлі : youtube.com/watch?v=Z_OAlIhXziw
jcugat

Відповіді:


570

Давайте спочатку вийдемо з речі. Пояснення, yield from gеквівалентне for v in g: yield v навіть, не починає справедливо ставитися до того yield from, про що йдеться. Тому що, давайте визнаємо, якщо все yield fromце розширює forцикл, то це не вимагає додавання yield fromдо мови і не дозволяє цілій купі нових функцій реалізовуватися в Python 2.x.

Що yield fromце робить, це встановлює прозорий двосторонній зв'язок між абонентом та підгенератором :

  • З'єднання є "прозорим" в тому сенсі, що він також поширюватиме все правильно, а не лише створені елементи (наприклад, поширюються винятки).

  • З'єднання «двунаправленное» в тому сенсі , що дані можуть бути і посланими від і до генератору.

( Якщо ми говорили про TCP, це yield from gможе означати "зараз тимчасово відключити сокет мого клієнта і підключити його до цього іншого серверного сокета". )

BTW, якщо ви не впевнені, що навіть означає передача даних в генератор , вам потрібно спочатку все скинути і прочитати про супроводи - вони дуже корисні (на відміну від підпрограм ), але, на жаль, менш відомі в Python. Цікавий курс Дейва Бізлі з питань судових процедур - чудовий початок. Прочитайте слайди 24-33 для швидкої грунтовки.

Зчитування даних з генератора, використовуючи вихід від

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

Замість того, щоб вручну повторювати reader(), ми можемо просто yield fromце зробити.

def reader_wrapper(g):
    yield from g

Це працює, і ми усунули один рядок коду. І, напевно, наміри трохи чіткіші (чи ні). Але нічого життя не змінюється.

Надсилання даних в генератор (супровід), використовуючи вихід із - Частина 1

Тепер давайте зробимо щось цікавіше. Створимо підпрограму під назвою, writerяка приймає надіслані їй дані та записує в сокет, fd тощо.

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

Тепер питання полягає в тому, як функція обгортки повинна обробляти надсилання даних письменнику, щоб будь-які дані, що надсилаються в обгортку, прозоро надсилалися до 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

Обгортка повинна прийняти дані, які надсилаються до неї (очевидно), а також повинна обробляти, StopIterationколи цикл for for вичерпаний. Очевидно, що просто робити for x in coro: yield xне обійдеться. Ось версія, яка працює.

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

Або ми могли це зробити.

def writer_wrapper(coro):
    yield from coro

Це економить 6 рядків коду, робить його набагато більш читабельним і він просто працює. Магія!

Надсилання даних генератору з - Частина 2 - Обробка виключень

Зробимо це складніше. Що робити, якщо нашому письменникові потрібно обробляти винятки? Скажімо, writerручка a SpamExceptionі вона друкує, ***якщо вона зустрічається.

class SpamException(Exception):
    pass

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

Що робити, якщо ми не змінимось writer_wrapper? Це працює? Спробуймо

# 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

Гм, це не працює, тому що x = (yield)просто збільшується виняток, і все припиняється. Давайте змусимо це працювати, але вручну обробляти винятки та надсилаючи їх чи кидаючи їх у підгенератор ( 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

Це працює.

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

Але так і відбувається!

def writer_wrapper(coro):
    yield from coro

В yield fromпрозоро ручки посилаючи значення або кидати значення в суб-генератора.

Однак це все ще не стосується всіх кутових справ. Що станеться, якщо зовнішній генератор закритий? Що щодо випадку, коли підгенератор повертає значення (так, в Python 3.3+, генератори можуть повертати значення), як слід поширювати повернене значення? Те, що yield fromпрозоро обробляє всі кутові корпуси, справді вражає . yield fromпросто магічно працює і обробляє всі ці справи.

Я особисто вважаю yield from, що це поганий вибір ключових слів, оскільки це не робить двостороннього характеру очевидним. Були запропоновані інші ключові слова (на кшталт, delegateале вони були відхилені, оскільки додати нове ключове слово до мови набагато складніше, ніж поєднувати наявні.

Підсумовуючи це, найкраще розглядати це yield fromяк transparent two way channelміж абонентом і підгенератором.

Список літератури:

  1. PEP 380 - Синтаксис делегування до підгенератора (Ewing) [v3.3, 2009-02-13]
  2. PEP 342 - Контроль за допомогою посилених генераторів (GvR, Eby) [v2.5, 2005-05-10]

3
@PraveenGollakota, у другій частині вашого запитання: Надіслати дані генератору (кореневі програми), використовуючи вихід із - Частина 1 , що робити, якщо у вас є більше, ніж супровід, щоб переслати отриманий товар? Як сценарій мовлення або передплатників, коли ви надаєте кілька примірників обгортки у своєму прикладі, а елементи повинні бути надіслані всім або їх підмножині?
Кевін Габусі

3
@PraveenGollakota, Kudos за чудову відповідь. Маленькі приклади дозволяють мені спробувати речі у відповіді. Посилання на курс Дейва Бізлі було бонусом!
BiGYaN

1
робити except StopIteration: passINSIDE while True:цикл не є точним поданням yield from coro- який не є нескінченним циклом і після того, як coroбуде вичерпаний (тобто підвищує StopIteration), writer_wrapperвиконає наступне твердження. Після останньої заяви він автоматично підніметься StopIterationяк будь-який виснажений генератор ...
Апр.

1
... тож якщо він writerміститься for _ in range(4)замість while True, то після друку >> 3він також буде автоматично підніматися, StopIterationі це буде автоматично оброблено, yield fromа потім writer_wrapperавтоматично підніме його власне, StopIterationі тому що wrap.send(i)це не всередині tryблоку, воно було б фактично підняте в цей момент ( тобто трекбек повідомляє лише про рядок із wrap.send(i)генератором, а не про все, що знаходиться всередині генератора)
Квільйон

3
Прочитавши « навіть не починає правосуддя », я знаю, що дійшов правильної відповіді. Дякую за чудове пояснення!
Hot.PxL

89

У яких ситуаціях корисний "вихід від"?

Кожна ситуація, коли у вас є такий цикл:

for x in subgenerator:
  yield x

Як описано в PEP, це досить наївна спроба використання підгенератора, у ньому відсутні кілька аспектів, особливо правильне поводження з .throw()/ .send()/ .close()механізмами, запровадженими PEP 342 . Для цього правильно потрібен досить складний код.

Який класичний випадок використання?

Подумайте, що ви бажаєте отримати інформацію з рекурсивної структури даних. Скажімо, ми хочемо, щоб усі дерева листя були на дереві:

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

Ще важливішим є той факт, що до цього yield fromчасу не існувало простого методу рефакторингу коду генератора. Припустимо, у вас є такий (безглуздий) генератор, як цей:

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)

Тепер ви вирішили розподілити ці петлі на окремі генератори. Без yield fromцього це некрасиво, аж до того, коли ви подумаєте двічі, чи дійсно ви хочете це зробити. З yield from, насправді приємно дивитися:

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)

Чому його порівняно з мікропотоками?

Я думаю, про що йдеться в цьому розділі в PEP - це те, що кожен генератор має свій окремий контекст виконання. Разом з тим , що виконання перемикається між генератором-ітератора і викликає абонентом , що використовують yieldі __next__(), відповідно, це схоже на нитки, в якому операційна система перемикає виконується потік , час від часу, поряд з контекстом виконання (стек, регістри, ...).

Ефект цього також порівнянний: і генератор-ітератор, і виклик одночасно прогресують у своєму стані виконання, їх виконання виконуються між собою. Наприклад, якщо генератор проводить якісь обчислення і абонент виводить результати, ви побачите результати, як тільки вони будуть доступні. Це форма одночасності.

Ця аналогія не є чимось конкретним yield from, проте - це загальна властивість генераторів Python.


Рефакторинг генераторів сьогодні болісний .
Джош Лі

1
Я схильний використовувати itertools дуже багато для рефакторингу генераторів (такі речі, як itertools.chain), це не велика справа. Мені подобається врожай, але я все ще не бачу, наскільки це революційно. Це, мабуть, так, оскільки Гвідо на це все божевільний, але, мабуть, не вистачає великої картини. Я думаю, що це чудово для send (), оскільки це важко для рефактора, але я не використовую це досить часто.
e-satis

Я припускаю, що get_list_values_as_xxxце прості генератори з однією лінією, for x in input_param: yield int(x)а інші два відповідно з strіfloat
madtyn

@NiklasB. повторно "витягнути інформацію з рекурсивної структури даних". Я просто заходжу в Py для даних. Не могли б ви зробити цей удар у цьому Q ?
alancalvitti

33

Де б ви викликаєте генератор зсередини генератора вам потрібно «прокачати» повторно yieldзначення: for v in inner_generator: yield v. Як зазначає ПЕП, є певні складності цього, які більшість людей ігнорує. Не локальний контроль потоку, як, наприклад, throw()є одним із прикладів, наведених у PEP. Новий синтаксис yield from inner_generatorвикористовується там, де ви раніше писали б явний forцикл. Хоча це не просто синтаксичний цукор: він обробляє всі кутові випадки, які ігноруються forциклом. Бути "цукристим" спонукає людей використовувати його і таким чином отримувати правильну поведінку.

Це повідомлення в дискусійній темі говорить про такі складності:

З додатковими функціями генератора, введеними PEP 342, це вже не так: як описано в PEP Грега, проста ітерація не підтримує send () і кидаю () правильно. Гімнастика, необхідна для підтримки send () та кидання (), насправді не така складна, коли ви їх ламаєте, але вони теж не тривіальні.

Я не можу говорити про порівняння з мікропотоками, крім того, щоб зауважити, що генератори - це тип паралелізму. Ви можете вважати підвісний генератор потоком, який надсилає значення yieldна споживчу нитку. Фактична реалізація може не бути подібною (а реальна реалізація, очевидно, представляє великий інтерес для розробників Python), але це не стосується користувачів.

Новий yield fromсинтаксис не додає мови додаткової можливості з точки зору нарізки, він просто полегшує правильне використання існуючих функцій. Або, якщо точніше, полегшує початківця споживача складного внутрішнього генератора, написаного експертом, пройти через цей генератор, не порушуючи жодної його складної характеристики.


23

Короткий приклад допоможе вам зрозуміти один із yield fromвипадків використання: отримати значення з іншого генератора

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]]])))

2
Просто хотів припустити, що друк в кінці виглядатиме трохи приємніше без перетворення на список -print(*flatten([1, [2], [3, [4]]]))
yoniLavi

6

yield from в основному ланцюги ітераторів є ефективними способами:

# 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

Як ви бачите, він видаляє одну чисту петлю Python. Це майже все, що він робить, але ланцюжок ітераторів є досить поширеною схемою в Python.

Нитки - це в основному функція, яка дозволяє вам вистрибувати з функцій у абсолютно випадкових точках і переходити до стану іншої функції. Контролер потоків робить це дуже часто, тому, здається, програма виконує всі ці функції одночасно. Проблема полягає в тому, що точки є випадковими, тому вам потрібно застосувати блокування, щоб запобігти зупинці функції керівника в проблемній точці.

У цьому сенсі генератори досить схожі на потоки: вони дозволяють вказувати конкретні точки (коли вони є yield), де можна заскакувати та вийти. При використанні цього способу генератори називаються кореневищами.

Прочитайте ці чудові підручники про супроводи в Python для отримання більш детальної інформації


10
Ця відповідь вводить в оману, оскільки вона усуває помітну особливість "дохідності", як згадувалося вище: send () і give () підтримка.
Джастін Ш

2
@Justin W: Я думаю, що все, що ви читали раніше, насправді вводить в оману, оскільки ви не зрозуміли, що throw()/send()/close()це yieldфункції, які, yield fromочевидно, повинні належним чином реалізувати, оскільки це має спростити код. Такі дрібниці не мають нічого спільного з використанням.
Jochen Ritzel

5
Ви оспорюєте відповідь Бена Джексона вище? Моє прочитання вашої відповіді полягає в тому, що це синтаксичний цукор, який слід перетворенням коду, який ви надали. Відповідь Бена Джексона конкретно спростовує це твердження.
Justin W

@JochenRitzel Ніколи не потрібно писати власну chainфункцію, оскільки вона itertools.chainвже існує. Використовуйте yield from itertools.chain(*iters).
Акумен

3

У застосованому використанні для асинхронної введення-виведення програми , yield fromмає аналогічну поведінку, як і awaitу функції кореневища . Обидва вони використовуються для призупинення виконання програми.

Для Asyncio, якщо немає необхідності підтримувати старішу версію Python (тобто> 3.5), async def/ awaitє рекомендований синтаксис для визначення супроводу. Таким чиномyield from , більше не потрібно в супровідній програмі.

Але в цілому за межі asyncio, yield from <sub-generator>є ще якесь - то інше використання в ітеріруя суб-генератор , як зазначено в попередньому відповіді.


1

Цей код визначає функцію, що fixed_sum_digitsповертає генератор, що перераховує всі шість цифр, таким чином, що сума цифр дорівнює 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) 

Спробуйте написати це без yield from. Якщо ви знайдете ефективний спосіб зробити це, дайте мені знати.

Я думаю, що для таких випадків: відвідування дерев yield fromробить код простішим та чистішим.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.