Является ли цикл через генератор в цикле над тем же генератором безопасным в Python?

из того, что я понимаю, a for x in a_generator: foo(x) цикл в Python примерно эквивалентен этому:

try:
    while True:
        foo(next(a_generator))
except StopIteration:
    pass

это говорит о том, что что-то вроде этого:

for outer_item in a_generator:
    if should_inner_loop(outer_item):
        for inner_item in a_generator:
            foo(inner_item)
            if stop_inner_loop(inner_item): break
    else:
        bar(outer_item)

сделал бы две вещи:

  1. не поднимать никаких исключений, segfault или что-нибудь в этом роде
  2. перебрать y пока он не достигнет некоторых x здесь should_inner_loop(x) возвращает truthy, затем цикл над ним во внутреннем for до stop_inner_loop(thing) возвращает true. Затем внешний цикл возобновляется где внутренний остановился.

из моих, по общему признанию, не очень хороших тестов, он, кажется, выполняет, как указано выше. Тем не менее, я не смог найти ничего в спецификации, гарантирующей, что это поведение является постоянным для интерпретаторов. Есть ли где-нибудь, что говорит или подразумевает, что я могу быть уверен, что это всегда будет так? Может ли это вызвать ошибки или выполнить каким-либо другим способом? (т. е. сделать что-то другое, чем описано выше


Б. Н. код эквивалентен выше взято из моего собственного опыта; я не знаю, действительно ли это точно. Вот почему я спрашиваю.

3 ответов


TL; DR: безопасно с CPython (но я не мог найти никакой спецификации этого), хотя он может не делать то, что вы хотите сделать.


во-первых, давайте поговорим о вашем первом предположении, эквивалентность.

цикл for фактически вызывает first iter() на объекте, затем выполняется next() по его результату, пока он не получит StopIteration.

вот соответствующий байт-код (низкоуровневая форма Python, используемая интерпретатором сама):

>>> import dis
>>> def f():
...  for x in y:
...   print(x)
... 
>>> dis.dis(f)
  2           0 SETUP_LOOP              24 (to 27)
              3 LOAD_GLOBAL              0 (y)
              6 GET_ITER
        >>    7 FOR_ITER                16 (to 26)
             10 STORE_FAST               0 (x)

  3          13 LOAD_GLOBAL              1 (print)
             16 LOAD_FAST                0 (x)
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 JUMP_ABSOLUTE            7
        >>   26 POP_BLOCK
        >>   27 LOAD_CONST               0 (None)
             30 RETURN_VALUE

GET_ITER звонки iter(y) (который сам называет y.__iter__()) и толкает свой результат в стек (подумайте об этом как о куче локальных неназванных переменных), а затем входит в цикл в FOR_ITER, которая называет next(<iterator>) (который сам называет <iterator>.__next__()), а затем выполняет код внутри цикла, а JUMP_ABSOLUTE делает исполнение возвращается к FOR_ITER.


теперь за безопасность:

вот методы генератора: https://hg.python.org/cpython/file/101404/Objects/genobject.c#l589 Как вы можете видеть на строка 617 реализация __iter__() и PyObject_SelfIter, выполнение которых вы можете найти здесь. PyObject_SelfIter просто возвращает объект (т. е. сам генератор).

Итак, когда вы вставляете два цикла, оба повторяются на одном и том же итераторе. И, как вы сказали, они просто зовут next() на нем, так что это безопасно.

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

если это не то, что вы хотите сделать, рассмотрим itertools.tee(), который буферизует вывод итератора, позволяя вам повторять его вывод дважды (или более). Это эффективно, только если итераторы tee остаются близко друг к другу в выходном потоке; если один итератор tee будет полностью исчерпан до использования другого, это лучше просто позвонить list на итераторе, чтобы материализовать список из него.


нет, это небезопасно (как в, мы не получим результат, который мы могли бы ожидать).

рассмотрим следующий пример:

a = (_ for _ in range(20))
for num in a:
    print(num)

конечно, мы получим от 0 до 19 печатных.

теперь давайте добавим немного кода:

a = (_ for _ in range(20))
for num in a:
    for another_num in a:
        pass
    print(num)

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

мы можем также сделать это:

a = (_ for _ in range(20))
for num in a:
    for another_num in a:
        print(another_num)

если бы это было безопасно, мы ожидали бы получить от 0 до 19 печатных 20 раз, но на самом деле мы печатаем его только один раз, по той же причине, о которой я упоминал выше.


это не совсем ответ на ваш вопрос, но я бы порекомендовал не делать этого, потому что код не читаемый. Мне потребовалось некоторое время, чтобы увидеть, что вы используете y дважды, хотя в этом весь смысл вашего вопроса. Не смущайте этим будущего читателя. Когда я вижу вложенную петлю, я не ожидаю того, что вы сделали, и мой мозг с трудом видит это.

Я бы сделал так:

def generator_with_state(y):
    state = 0
    for x in y:
        if isinstance(x, special_thing):
            state = 1
            continue
        elif state == 1 and isinstance(x, signal):
            state = 0
        yield x, state

for x, state in generator_with_state(y):
    if state == 1:
        foo(x)
    else:
        bar(x)