2013-08-28 2 views
4

Следующий код python производит [(0, 0), (0, 7) ... (0, 693)] вместо ожидаемого списка кортежей, объединяющего все кратные 3 и кратные 7:Несогласованное поведение генераторов питона

multiples_of_3 = (i*3 for i in range(100)) 
multiples_of_7 = (i*7 for i in range(100)) 
list((i,j) for i in multiples_of_3 for j in multiples_of_7) 

Этот код исправляет проблему:

list((i,j) for i in (i*3 for i in range(100)) for j in (i*7 for i in range(100))) 

Вопросы:

  1. объект генератор, кажется, играет роль итератора я вместо предоставления объекта итератора каждый раз, когда должен быть указан список сгенерированный. Более поздняя стратегия, по-видимому, принимается объектами запроса .Net LINQ. Есть ли элегантный способ обойти это?
  2. Как работает вторая часть кода? Должен ли я понять, что итератор генератора не сбрасывается после прохождения через все кратные 7?
  3. Вам не кажется, что это поведение противоречит интуитивному, если не противоречивому?
+0

Read: [Ключевое слово урока Python объяснено] (http://stackoverflow.com/questions/231767/the-python-yield-keyword-explained). – Bakuriu

ответ

2

Как вы обнаружили объект, созданный выражением генератора итератор (точнее генератор итератора), предназначенный для потребления только один раз. Если вам нужен сбрасываемый генератор, просто создать реальный генератор и использовать его в петлях:

def multiples_of_3():    # generator 
    for i in range(100): 
     yield i * 3 
def multiples_of_7():    # generator 
    for i in range(100): 
     yield i * 7 
list((i,j) for i in multiples_of_3() for j in multiples_of_7()) 

Ваш второй код работает, потому что список выражений внутреннего цикла ((i*7 ...)) вычисляется на каждом проходе внешнего контура , Это приводит к созданию нового генератора-итератора каждый раз, что дает вам поведение, которое вы хотите, но за счет ясности кода.

Чтобы понять, что происходит, помните, что нет «перезагрузки» итератора, когда цикл for выполняет итерацию по нему. (Это функция, например сброс сломается итерация большой итератора в части, и это было бы невозможно для генераторов). Например:

multiples_of_2 = iter(xrange(0, 100, 2)) # iterator 
for i in multiples_of_2: 
    print i 
# prints nothing because the iterator is spent 
for i in multiples_of_2: 
    print i 

... в отличие от этого:

multiples_of_2 = xrange(0, 100, 2)  # iterable sequence, converted to iterator 
for i in multiples_of_2: 
    print i 
# prints again because a new iterator gets created 
for i in multiples_of_2: 
    print i 

Генерирующее выражение эквивалентно вызываемому генератору и поэтому может быть только повторено за один раз.

+0

Вам не кажется, что генераторы были бы более полезны, если бы они были итераторами вместо итераторов? На самом деле это то, что беспокоит меня больше всего. Я возвращаюсь от экспериментов с Haskell и LINQ, которые делают ленивые оценки, тем самым экономя память в процессе. – Tarik

+0

Функции генератора * являются * итерабельными в общем смысле слова - вы конвертируете их в итератор, вызывая их. Если в «генераторах» вы ссылаетесь на выражения * генератора *, они, безусловно, могли быть реализованы как итераторы, чья '__iter__' вызывает невидимую базовую функцию генератора и создает новый генератор-итератор. Но * это * было бы несовместимо с тем, как работают обычные генераторы. (Генерирующие выражения были введены после регулярных функций генератора - 'def', которые содержат 'yield' - уже были частью языка.) – user4815162342

+0

Поскольку генератор является просто синтаксическим сахаром для того, что на самом деле реализуется базовым def , что сделало бы это непоследовательным, если бы оно было реализовано с использованием def, содержащего доходность. Посмотрите на лямбда-выражения: разве они не реализованы как def за кулисами? Опять же, просто синтаксический сахар. Суть в том, что я до сих пор не понимаю, что мы потеряем, если генераторы будут реализованы как итераторы вместо итераторов. – Tarik

3

Объект-генератор is итератор, и поэтому одноразовый. Это не итерируемый, который может производить любое количество независимых итераторов. Такое поведение не является чем-то, что вы можете изменить с помощью коммутатора где-то, поэтому любая работа вокруг сводится к использованию либо итерации (например, списка), а не генератора или многократного создания генераторов.

Второй фрагмент последнего. Это по определению эквивалентно петлях

for i in (i*3 for i in range(100)): 
    for j in (i*7 for i in range(100)): 
     ... 

Надеюсь, это не удивительно, что здесь, выражение последнего генератора вычисляется заново на каждой итерации внешнего цикла.

+0

«Объект-генератор - это итератор, и поэтому один-выстрел. Это не итерируемый, который может производить любое количество независимых итераторов». +1 для этого. Я как бы понял это, задав свой вопрос. Однако, пожалуйста, проверьте мой обновленный вопрос в соответствии с комментарием, который я только что разместил в ответ user4815162342. – Tarik

+0

@Tarik Это немного субъективно. Я вижу, что это запутывание происходит из LINQ, но по моему опыту это довольно легко избежать - я знаю, что он работает именно так, но я не могу вспомнить, что нужно писать код по-другому из-за этого факта. Я полагаю, что это проще, если один * вручную * продвигает итератор. Например, рассмотрим 'next (g); для x в g: ... ', чтобы пропустить первый элемент - для этого требуется хотя бы одна дополнительная строка, если' iter (g) не является g'. Обратите внимание, что все остальные итераторы ведут себя одинаково, по лучшим причинам, поэтому изменение выражений генератора не устранит эту ошибку полностью. – delnan

0

Реальная проблема, как я узнал о одного против нескольких итерируемымих пропусков и того факта, что в настоящее время отсутствует стандартный механизм, чтобы определить, является ли итератор одного или несколько прохода: См Single- vs. Multi-pass iterability

1

Если вы хотите, чтобы преобразовать выражение генератора для многопроходного повтора, тогда это можно сделать довольно обычным способом. Например:

class MultiPass(object): 
    def __init__(self, initfunc): 
     self.initfunc = initfunc 
    def __iter__(self): 
     return self.initfunc() 

multiples_of_3 = MultiPass(lambda: (i*3 for i in range(20))) 
multiples_of_7 = MultiPass(lambda: (i*7 for i in range(20))) 
print list((i,j) for i in multiples_of_3 for j in multiples_of_7) 

С точки зрения определения вещи, это такое же количество работы, набрав:

def multiples_of_3(): 
    return (i*3 for i in range(20)) 

, но с точки зрения пользователя, они пишут multiples_of_3, а не multiples_of_3(), что означает, что объект multiples_of_3 является полиморфным с любым другим итерируемым, например tuple или list.

Необходимо ввести lambda: немного неэлегантно, правда. Я не думаю, что было бы вредно вводить «итерируемые понимания» на язык, чтобы дать вам то, что вы хотите, сохраняя при этом обратную совместимость. Но есть только так много символов пунктуации, и я сомневаюсь, что это будет считаться заслуживающим внимания.

+0

Спасибо за добавление интересных моментов в эту дискуссию. Хотя я имел это решение в виду, я по-прежнему считаю, что итераторы должны быть многопроходными, в то время как итераторы должны быть однопроходными, независимо от того, как они создаются (лямбда-выражения, функции генератора, выражение генератора ...) – Tarik

+0

@ Тарик: на самом деле это невозможно , поскольку вы хотите иметь возможность поставлять вместо итеративного, то, что по сути является однопроходным (например, файл-объект, считанный из stdin или сокета). Вот почему итераторы сами по себе являются итерируемыми (в некоторых случаях просто возвращающими «я»), чтобы поддерживать этот полиморфизм. –

Смежные вопросы