Насколько я понимаю, когда запускается такой код:

for i in MyObject:
    print(i)

Функция __iter__ в MyObject запускается, и цикл for использует возвращаемый итератор для запуска цикла.

Возможно ли получить доступ к этому среднему циклу объекта итератора? Это скрытая локальная переменная или что-то в этом роде?

Я бы хотел сделать следующее:

for i in MyObject:
    blah = forloopiterator()
    modify_blah(blah)
    print(i)

Я хочу сделать это, потому что я строю отладчик, и мне нужно изменить итератор после его создания (добавление объекта, который будет повторяться во время этого цикла, в середине исполнения). Я знаю, что это взломать и не должно быть сделано условно. Изменение MyObject.items (что итератор повторяет) напрямую не работает, так что итератор вычисляет только один раз. Поэтому мне нужно изменить итератор напрямую.

2
Daniel Paczuski Bak 20 Авг 2018 в 22:38

5 ответов

Лучший ответ

можно делать то, что вы хотите делать, если вы готовы полагаться на несколько недокументированных внутренних элементов вашего интерпретатора Python (в моем случае, CPython 3.7), но это не так сделать тебе что-нибудь хорошее.


Итератор не доступен ни locals, ни где-либо еще (даже отладчику). Но как указал Патрик Хау, вы можете получить по адресу косвенно, через get_referrers. Например:

for ref in gc.get_referrers(seq):
    if isinstance(ref, collections.abc.Iterator):
        break
else:
    raise RuntimeError('Oops')

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


Теперь, что вы делаете с этим? У вас есть итератор для seq, и ... что теперь? Вы не можете заменить это чем-то полезным, например itertools.chain(seq, [1, 2, 3]). Нет общедоступного API для итераторов списков, множеств и т. Д., А тем более произвольных итераторов.

Если вы знаете, что это итератор списка… ну, CPython 3.x listiterator действительно изменчив. Они выбираются путем создания пустого итератора и вызова __setstate__ со ссылкой на список и индекс:

>>> print(ref.__reduce__())
(<function iter>, ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],), 7)
>>> ref.__setstate__(3) # resets the iterator to index 3 instead of 7
>>> ref.__reduce__()[1][0].append(10) # adds another value

Но это все глупо, потому что вы можете получить тот же эффект, просто изменив первоначальный список. По факту:

>>> ref.__reduce__()[1][0] is seq
True

Так:

lst = list(range(10))
for elem in lst:
  print(elem, end=' ')
  if elem % 2:
    lst.append(elem * 2)
print()

… Распечатает:

0 1 2 3 4 5 6 7 8 9 2 6 10 14 18 

... без необходимости использовать итератор.


Вы не можете сделать то же самое с сетом.

Отключение набора во время его итерации повлияет на итератор так же, как изменение списка - но то, что он делает, не определено. В конце концов, наборы имеют произвольный порядок, который гарантированно будет постоянным , пока вы не добавите или не удалите . Что произойдет, если вы добавите или удалите в середине? Вы можете получить совершенно другой порядок, то есть вы можете повторить элементы, которые вы уже повторяли, и пропустить элементы, которые вы никогда не видели. Python подразумевает, что это должно быть недопустимо в любой реализации, и CPython действительно проверяет это:

s = set(range(10))
for elem in s:
  print(elem, end=' ')
  if elem % 2:
    s.add(elem * 2)
print()

Это сразу же поднимет:

RuntimeError: Set changed size during iteration

Итак, что произойдет, если мы воспользуемся тем же приемом, чтобы пойти за спиной Python, найти set_iterator и попытаться изменить его?

s = {1, 2, 3}
for elem in s:
    print(elem)
    for ref in gc.get_referrers(seq):
        if isinstance(ref, collections.abc.Iterator):
            break
    else:
        raise RuntimeError('Oops')
    print(ref.__reduce__)

То, что вы увидите в этом случае, будет примерно таким:

2
(<function iter>, ([1, 3],))
1
(<function iter>, ([3],))
3
(<function iter>, ([],))

Другими словами, когда вы выбираете set_iterator, он создает список оставшихся элементов и возвращает вам инструкции по созданию нового списка из этого списка. Отмена этого временного списка, очевидно, не имеет никакого полезного эффекта.


Как насчет кортежа? Очевидно, вы не можете просто мутировать сам кортеж, потому что кортежи неизменны. Но как насчет итератора?

Под прикрытием в CPython tuple_iterator имеет ту же структуру и код, что и listiterator (как и тип iterator, который вы получаете, вызывая iter в «старом стиле» тип sequence, который определяет __len__ и __getitem__, но не __iter__). So, you can do the exact same trick to get at the iterator, and to уменьшают `it.

Но как только вы это сделаете, ref.__reduce__()[1][0] is seq снова станет правдой - другими словами, это кортеж, тот же кортеж, который у вас уже был, и все еще неизменный.

4
abarnert 20 Авг 2018 в 21:01

Если вы хотите вставить дополнительный объект в середине итерации цикла в отладчике, вам не нужно делать это путем изменения итератора. Вместо этого, после окончания цикла, перейдите к первой строке тела цикла, а затем установите переменную цикла для нужного вам объекта. Вот пример PDB. Со следующим файлом:

import pdb

def f():
    pdb.set_trace()
    for i in range(5):
        print(i)
f()

Я записал сеанс отладки, который вставляет 15 в цикл:

> /tmp/asdf.py(5)f()
-> for i in range(5):
(Pdb) n
> /tmp/asdf.py(6)f()
-> print(i)
(Pdb) n
0
> /tmp/asdf.py(5)f()
-> for i in range(5):
(Pdb) j 6
> /tmp/asdf.py(6)f()
-> print(i)
(Pdb) i = 15
(Pdb) n
15
> /tmp/asdf.py(5)f()
-> for i in range(5):
(Pdb) n
> /tmp/asdf.py(6)f()
-> print(i)
(Pdb) n
1
> /tmp/asdf.py(5)f()
-> for i in range(5):
(Pdb) c
2
3
4

(Из-за ошибки PDB вы должны перейти, затем установить переменную цикла. PDB потеряет изменение в переменной цикла, если вы перейдете сразу после установки.)

0
user2357112 supports Monica 20 Авг 2018 в 20:33

Если вам не известен отладчик pdb в python, попробуйте. Это очень интерактивный отладчик, с которым я когда-либо сталкивался.

отладчик python

Я уверен, что мы можем контролировать итерации цикла вручную с помощью pdb. Но изменить список в середине, не уверен. Попробуйте.

-2
Jim Todd 20 Авг 2018 в 20:21

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

>>> it = iter(MyObject)
>>> it.next()
-3
Spack 20 Авг 2018 в 19:41

Нет, получить доступ к этому итератору невозможно (разве что с помощью Python C API, но это только предположение). Если вам это нужно, присвойте его переменной перед циклом.

it = iter(MyObject)
for i in it:
  print(i)
  # do something with it

Помните, что продвижение итератора вручную может вызвать исключение StopIteration.

for i in it:
  if check_skip_next_element(i):
    try: next(it)
    except StopIteration: break

Использование break является предметом обсуждения. В этом случае он имеет ту же семантику, что и continue, но вы можете просто использовать pass, если хотите продолжать работу до конца блока for.

3
Niklas R 20 Авг 2018 в 19:41
51937482