Подтвердить что ты не робот

Выход в представлениях списков и выражений генератора

Следующее поведение кажется мне довольно противоречивым (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)

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

Кроме того, кажется странным

  • что понимание списка возвращает генератор, а не список
  • и что выражение генератора, преобразованное в список и соответствующее понимание списка, содержит разные значения.

Может ли кто-нибудь предоставить дополнительную информацию?

4b9b3361

Ответ 1

Выражения генератора, а также выражения set и dict компилируются в объекты функции (генератора). В 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 (поэтому компилятор кода), но спецификация выражения yield показывает, что использование yield здесь не должно работать:

Выражение yield используется только при определении функции генератора и, следовательно, может использоваться только в теле определения функции.

Это уже привело к запутывающим ошибкам, где кто-то пытался использовать yield в функции генератора внутри выражения генератора, ожидая применения yield к функции. Разработчики Python знают об этой проблеме, поскольку запись Guido о записи не предназначена:

Я думаю, что это определенно неправильно, как это работает в 3.x. (Тем более, что он работает как ожидается в 2.x.)

Я согласен с предпочтениями Inyeol исправлений: (1) заставить его работать правильно для listcomps, а также genexps, (2) если это невозможно, запретите выход в genexp или listcomp.

Различия между тем, как 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])]