2016-12-22 6 views
5

Я пытаюсь сгладить вложенную генератор генераторов, но я получаю неожиданный результат:Сведения вложенной генератор выражений

>>> g = ((3*i + j for j in range(3)) for i in range(3)) 
>>> list(itertools.chain(*g)) 
[6, 7, 8, 6, 7, 8, 6, 7, 8] 

Я ожидал, что результат выглядит следующим образом:

[0, 1, 2, 3, 4, 5, 6, 7, 8] 

Я думаю, что получаю неожиданный результат, потому что внутренние генераторы не оцениваются до тех пор, пока внешний генератор уже не будет повторен, установив i на 2. Я могу взломать решение, заставив оценку внутренних генераторов, используя понимание списка вместо генного Выражение Erator:

>>> g = ([3*i + j for j in range(3)] for i in range(3)) 
>>> list(itertools.chain(*g)) 
[0, 1, 2, 3, 4, 5, 6, 7, 8] 

В идеале я хотел бы решение, которое полностью ленивый и не принуждать оценку внутренних вложенных элементов, пока они не используются.

Есть ли способ сгладить вложенные выражения генератора произвольной глубины (возможно, используя что-то, кроме itertools.chain)?

Edit:

Нет, мой вопрос не является дубликатом Variable Scope In Generators In Classes. Я честно не могу сказать, как эти два вопроса связаны вообще. Может быть, модератор мог бы объяснить, почему он думает, что это дубликат.

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

def flattened1(iterable): 
    iter1, iter2 = itertools.tee(iterable) 
    if isinstance(next(iter1), collections.Iterable): 
     return flattened1(x for y in iter2 for x in y) 
    else: 
     return iter2 

def flattened2(iterable): 
    iter1, iter2 = itertools.tee(iterable) 
    if isinstance(next(iter1), collections.Iterable): 
     return flattened2(itertools.chain.from_iterable(iter2)) 
    else: 
     return iter2 

Насколько я могу сказать с timeit, они оба выполняют одинаково.

>>> timeit(test1, setup1, number=1000000) 
18.173431718023494 
>>> timeit(test2, setup2, number=1000000) 
17.854709611972794 

Я не уверен, какой из них лучше с точки зрения стиля либо, так как это x for y in iter2 for x in y немного обманщик мозга, но, возможно, более элегантно, чем itertools.chain.from_iterable(iter2). Вход оценивается.

К сожалению, я был в состоянии только отметить один из двух одинаково хороших ответов.

ответ

6

Вместо использования chain(*g), вы можете использовать chain.from_iterable:

>>> g = ((3*i + j for j in range(3)) for i in range(3)) 
>>> list(itertools.chain(*g)) 
[6, 7, 8, 6, 7, 8, 6, 7, 8] 
>>> g = ((3*i + j for j in range(3)) for i in range(3)) 
>>> list(itertools.chain.from_iterable(g)) 
[0, 1, 2, 3, 4, 5, 6, 7, 8] 
2

Как об этом:

[x for y in g for x in y] 

Что дает:

[0, 1, 2, 3, 4, 5, 6, 7, 8] 
1

Угадайте у вас уже есть свой ответ, но вот еще одна перспектива.

Проблема заключается в том, что при создании каждого внутреннего генератора выражение, генерирующее значение, закрывается по внешней переменной i, поэтому даже когда первый внутренний генератор начинает генерировать значения, он использует «текущее» значение i. Это будет иметь значение i=2, если внешний генератор был полностью потреблен (и это точно так же, как только аргумент в вызове chain(*g) оценивается, прежде чем chain фактически вызван).

Следующая коварный трюк будет работать вокруг проблемы:

g = ((3*i1 + j for i1 in [i] for j in range(3)) for i in range(3)) 

Заметим, что эти внутренние генераторы не закрыты над i, поскольку for положения оцениваются в момент создания генератора, так что список одноточечно [i] является и его значение «заморожено» перед лицом дальнейших изменений на значение i.

Этот подход имеет преимущество перед ответом from_iterable, что он немного более общий, если вы хотите использовать его вне вызова chain.from_iterable - он всегда будет производить «правильные» внутренние генераторы, независимо от того, является ли внешний генератор частично или полностью потребляется до использования внутренних генераторов. Например, в следующем коде:

g = ((3*i1 + j for i1 in [i] for j in range(3)) for i in range(3)) 
g1 = next(g) 
g2 = next(g) 
g3 = next(g) 

вы можете вставить следующие строки:

list(g1) 
list(g2) 
list(g3) 

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

+0

+1 Это действительно подлый и действительно классный, но это не помогает мне сгладить вложенный генератор, с которого я начал работать. –

+0

Нет, я думаю, ты прав! Хотя, если вы строите вложенные генераторы таким образом, чтобы значения внутренних генераторов зависели от времени оценки внешних генераторов, это программируемая бомба замедленного действия - некоторая мелкая рефакторинг кода приведет к взрыву всего. Может быть хорошей идеей перепроектировать внутренние генераторы, чтобы избежать нежелательного захвата переменной, независимо от того, как вы решили сгладить 'g' в конце. –

+0

Все, что вы сказали, верно, но моя (неустановленная) цель состоит не в том, чтобы использовать генераторы, подобные этим везде ('range()' генерирует сглаженные диапазоны чисел просто отлично), но для того, чтобы другие программисты могли определять многомерные массивы с использованием генераторов. Если они думают, что их многомерный массив выглядит как [[0,1], [2,3]], когда на самом деле он выглядит как [[2,3], [2,3]] - все без каких-либо ошибок - то я прищурился. –