вихід у розуміннях списку та виразах генератора


76

Наступна поведінка мені здається досить неінтуїтивною (Python 3.4):

>>> [(yield i) for i in range(3)]
<generator object <listcomp> at 0x0245C148>
>>> list([(yield i) for i in range(3)])
[0, 1, 2]
>>> list((yield i) for i in range(3))
[0, None, 1, None, 2, None]

Проміжні значення останнього рядка насправді не завжди None, це те, що ми sendвводимо в генератор, еквівалентно (я думаю) наступному генератору:

def f():
   for i in range(3):
      yield (yield i)

Мені здається смішним, що ці три рядки взагалі працюють. Посилання каже , що yieldдозволено тільки у визначенні функції (хоча я можу читати це неправильно і / або це може бути просто скопійовано зі старої версії). Перші два рядки створюють a SyntaxErrorв Python 2.7, а третій - ні.

Крім того, це здається дивним

  • що розуміння списку повертає генератор, а не список
  • і що вираз генератора, перетворений у список, і відповідне розуміння списку містять різні значення.

Хтось може надати більше інформації?

Відповіді:


75

Примітка : це була помилка в обробці CPython yieldв розуміннях та виразах генератора, виправлена ​​в Python 3.8, із попередженням про припинення використання в Python 3.7. Див. Звіт про помилки Python та записи Нові для Python 3.7 та Python 3.8 .

Вирази генератора, а також розуміння множин і диктів компілюються до об'єктів функції (генератора). У Python 3 розуміння списку отримує однакову обробку; всі вони, по суті, є новим вкладеним обсягом.

Ви можете переконатися в цьому, якщо спробувати розібрати вираз генератора:

>>> dis.dis(compile("(i for i in range(3))", '', 'exec'))
  1           0 LOAD_CONST               0 (<code object <genexpr> at 0x10f7530c0, file "", line 1>)
              3 LOAD_CONST               1 ('<genexpr>')
              6 MAKE_FUNCTION            0
              9 LOAD_NAME                0 (range)
             12 LOAD_CONST               2 (3)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               3 (None)
             26 RETURN_VALUE
>>> dis.dis(compile("(i for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                11 (to 17)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 POP_TOP
             14 JUMP_ABSOLUTE            3
        >>   17 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Вищезазначене показує, що вираз генератора компілюється до об'єкта коду, завантаженого як функція ( MAKE_FUNCTIONстворює об'єкт функції з об'єкта коду). .co_consts[0]Посилання дозволяє нам побачити об'єкт коду , згенерованого для вираження, і він використовує YIELD_VALUEтільки як функцію генератора буде.

Таким чином, yieldвираз працює в цьому контексті, оскільки компілятор розглядає їх як маскувальні функції.

Це помилка; yieldне має місця в цих виразах. Граматика Python перед Python 3.7 це дозволяє (саме тому код можна компілювати), але yieldспецифікація виразу показує, що використання yieldтут насправді не повинно працювати:

Вираз yield використовується лише при визначенні функції генератора і, отже, може використовуватися лише в тілі визначення функції.

Це було підтверджено помилкою у випуску 10544 . Дозвіл помилки в тому , що з допомогою yieldі yield fromбуде підняти SyntaxErrorв Python 3.8 ; в Python 3.7 він піднімає a,DeprecationWarning щоб переконатися, що код припиняє використання цієї конструкції. Ви побачите те саме попередження в Python 2.7.15 та новіших версіях, якщо ви використовуєте -3перемикач командного рядка, що вмикає попередження про сумісність Python 3.

Попередження 3.7.0b1 виглядає так; перетворення попереджень на помилки дає SyntaxErrorвиняток, як у 3.8:

>>> [(yield i) for i in range(3)]
<stdin>:1: DeprecationWarning: 'yield' inside list comprehension
<generator object <listcomp> at 0x1092ec7c8>
>>> import warnings
>>> warnings.simplefilter('error')
>>> [(yield i) for i in range(3)]
  File "<stdin>", line 1
SyntaxError: 'yield' inside list comprehension

Відмінності між тим, як yieldфункціонують розуміння списку та yieldгенераторський вираз, походять із різниці в тому, як реалізовані ці два вирази. У Python 3 розуміння списку використовує LIST_APPENDвиклики, щоб додати верх стека до побудованого списку, тоді як вираз генератора натомість дає це значення. Додавання (yield <expr>)просто додає ще один YIELD_VALUEкод операції до будь-якого:

>>> dis.dis(compile("[(yield i) for i in range(3)]", '', 'exec').co_consts[0])
  1           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                13 (to 22)
              9 STORE_FAST               1 (i)
             12 LOAD_FAST                1 (i)
             15 YIELD_VALUE
             16 LIST_APPEND              2
             19 JUMP_ABSOLUTE            6
        >>   22 RETURN_VALUE
>>> dis.dis(compile("((yield i) for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 YIELD_VALUE
             14 POP_TOP
             15 JUMP_ABSOLUTE            3
        >>   18 LOAD_CONST               0 (None)
             21 RETURN_VALUE

YIELD_VALUEОпкод в байткод індексів 15 і 12 , відповідно , є додатковою, зозуля в гнізді. Отже, для генератора списку-розуміння, що перетворився, у вас є 1 вихід, який кожного разу створює верх стека (замінюючи верх стека на yieldзначення, що повертається), а для варіанта виразу генератора ви отримуєте верх стека ( ціле число), а потім знову приносять результат , але тепер стек містить повернене значення, yieldі ви отримуєте Noneце вдруге.

Для розуміння списку тоді передбачуваний listвихідний об'єкт все ще повертається, але Python 3 розглядає це як генератор, тому натомість повернене значення приєднується до StopIterationвинятку як valueатрибут:

>>> from itertools import islice
>>> listgen = [(yield i) for i in range(3)]
>>> list(islice(listgen, 3))  # avoid exhausting the generator
[0, 1, 2]
>>> try:
...     next(listgen)
... except StopIteration as si:
...     print(si.value)
... 
[None, None, None]

Ці Noneоб'єкти - це повернені значення з yieldвиразів.

І ще раз повторити це; це саме питання стосується також розуміння словника та набору в Python 2 та Python 3; у Python 2 yieldповернені значення все ще додаються до передбачуваного словника або об'єкта набору, і повертане значення остаточно `` видається '', а не приєднується до StopIterationвинятку:

>>> list({(yield k): (yield v) for k, v in {'foo': 'bar', 'spam': 'eggs'}.items()})
['bar', 'foo', 'eggs', 'spam', {None: None}]
>>> list({(yield i) for i in range(3)})
[0, 1, 2, set([None])]

Зверніть увагу, що відповідно до специфікації мови, yield-atomце дозволено всередині виразу (всередині функції генератора). Це може бути ще більш проблематичним, якщо його yield-atomякось неправильно реалізувати.
skyking

1
@skyking: ось що я кажу; граматика це дозволяє. Помилка, на яку я посилаюся, намагається використовувати a yield як частину виразу генератора всередині функції генератора , де очікується, що yieldзастосовується до функції генератора, а не до вкладеної області виразу генератора.
Мартін Пітерс

Ого. Дійсно дуже інформативно. Отже, якщо я правильно зрозумів, сталося наступне: функція, яка містить і те, yieldі інше return, як це задокументовано, повинна стати функцією генератора, returnзначення ed якої повинно потрапити у StopIterationвиняток, і байт-код для розуміння списку з yieldвнутрішнім виглядом (хоча це було не призначений), як і байт-код такої функції.
zabolekar

@zabolekar: щось подібне; кроки приблизно такі: компілятор натрапляє на розуміння списку, тому будує об’єкт коду; компілятор натрапляє на yieldвираз, тому позначає поточний об'єкт коду як генератор. Вуаля, у нас є функція генератора.
Мартін Пітерс

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