在'for in'循环中访问迭代器

3

据我所知,当运行以下代码时:

for i in MyObject:
    print(i)

当运行MyObject的__iter__函数时,for循环使用它返回的迭代器来运行循环。

在循环中途访问此迭代器对象是否可能?它是一个隐藏的局部变量还是类似于那样的东西?

我想要做以下操作:

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

我希望这样做是因为我正在构建一个调试器,需要在实例化后修改迭代器(在执行期间添加一个对象进行迭代)。我知道这是一种hack方法,不应该常规使用。直接修改MyObject.items(迭代器正在迭代的内容)并不起作用,因为迭代器只评估一次。所以我需要直接修改迭代器。


3
请说明你试图做什么。否则,这可能会遭受任何XY问题的命运。 - Arthur Dent
2
"MyObject的__iter__函数被执行"这并不完全正确。更准确地说,应该是iter(MyObject)。对象可以在没有定义__iter__方法的情况下成为可迭代对象(通过定义__getitem__方法)。 - Aran-Fey
1
没有可靠的方法来访问它。 - Aran-Fey
2
@DanielPaczuskiBak 不,那不是真的。对于大多数类型(包括所有内置集合),修改对象确实会修改迭代的内容。例如,内置的listiterator保留了列表的引用和索引的等效值。(它可能被优化为更有效的东西,比如指向列表内部存储的中间加上针对重新分配内存的保护,但它必须像持有一个列表和一个索引一样运作。) - abarnert
1
如果你不小心删除了这个列表,你的解释器可能会崩溃——但是没有办法在不深入解释器的情况下删除这个列表。只有 del lst 并不能删除这个列表,它只是删除了一个指向列表值的引用变量。如果还有其他引用(比如列表迭代器中的引用),那么这个列表就不是垃圾,因此也不会被删除。 - abarnert
显示剩余12条评论
5个回答

5
不可能访问这个迭代器(除非使用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 具有相同的语义,但如果你想一直执行到 for 块的结尾,可以使用 pass


我正在尝试编写一个可以插入现有代码的调试器;我无法在循环之前添加任何内容,但我可以传递一个函数,在循环期间调用该函数,希望该函数可以在循环中修改迭代器。你确定这是不可能的吗? - Daniel Paczuski Bak
我正在尝试在循环中间添加新的MyObject项目,并确保它们被迭代。 - Daniel Paczuski Bak
1
你可以使用 next(it, None) 来避免 StopIteration - Carl Walsh

5

只要您愿意依靠Python解释器的多个未记录内部,就可以做到您想做的事情(在我的情况下,是CPython 3.7),但这并没有什么好处。


迭代器不会暴露给locals或其他任何地方(甚至不会暴露给调试器)。但正如Patrick Haugh所指出的那样,您可以通过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确实是可变的。它们被pickle的方式是创建一个空迭代器,并调用__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 

...无需与迭代器打交道。


使用set无法做到同样的事情。

当您在迭代集合时对其进行修改将影响迭代器,就像修改列表一样——但它的行为是不确定的。毕竟,集合具有任意顺序,只有在您不添加或删除元素时才能保证顺序一致。如果在中间添加或删除元素会发生什么?你可能会得到完全不同的顺序,这意味着你可能会重复已经迭代过的元素,并错过从未看到的元素。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>, ([],))

换句话说,当你使用pickle来序列化一个set_iterator时,它会创建一个剩余元素的列表,并返回用于构建新列表迭代器的指令。对临时列表进行变异显然没有任何有用的效果。
那么元组呢?显然,你不能直接变异元组本身,因为元组是不可变的。但是迭代器呢?
在CPython中,tuple_iterator共享与listiterator相同的结构和代码(调用iter方法并定义__len__和__getitem__但未定义__iter__的“旧样式序列”类型也是如此)。因此,你可以使用完全相同的技巧来获取迭代器并缩小它。
但是一旦你这样做了,ref.__reduce__()[1][0] is seq就会再次成立——换句话说,它是一个元组,与你已经拥有的相同元组,仍然是不可变的。

它实际上是一个元组,而不是一个集合(我的错误),我正在处理一个新问题。 - Daniel Paczuski Bak
2
@DanielPaczuskiBak 一个tuple_iterator会有一个不同的问题。在底层,它的工作方式与listiterator完全相同,只是它引用的对象是一个元组。显然,你无法改变这个元组。 - abarnert

0
如果您想在调试器中在迭代过程中插入一个额外的对象,您不需要通过修改迭代器来实现。相反,在循环结束后,跳转到循环体的第一行,然后将循环变量设置为您想要的对象。以下是一个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将会丢失对循环变量的更改。)


-2

如果你不知道Python中的pdb调试器,请尝试一下。这是我遇到过的最交互式的调试器。

Python调试器

我相信我们可以使用pdb手动控制循环迭代。但是在中途更改列表,我不确定。试试看吧。


如果您确定我们可以使用pdb手动控制循环迭代,请展示如何操作。但我认为您无法这样做(除非可能通过操纵当前帧对象的属性之类的方式)。for语句内部的迭代器对于pdb中的本地变量和原始代码中的本地变量来说都是不可见的。 - abarnert

-3

要访问给定对象的迭代器,您可以使用内置函数iter()

>>> it = iter(MyObject)
>>> it.next()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接