itertools 和步长列表赋值

3 投票
5 回答
826 浏览
提问于 2025-04-17 07:26

给定一个列表,比如 x = [True]*20,我想把每隔一个元素的值改成 False

x[::2] = False

但是这样做会报错,错误信息是 TypeError: must assign iterable to extended slice,意思是你必须给这个切片赋值一个可迭代的对象。

所以我天真地以为可以这样做:

x[::2] = itertools.repeat(False)

或者

x[::2] = itertools.cycle([False])

不过,按照我的理解,这样会导致一个无限循环。为什么会出现无限循环呢?有没有什么其他的方法可以在不事先知道切片中元素数量的情况下进行赋值呢?

补充说明:我明白 x[::2] = [False] * len(x)/2 在这种情况下是有效的,或者你可以在更一般的情况下为右侧的乘数想出一个表达式。我想弄清楚为什么 itertools 会无限循环,以及为什么列表赋值和 numpy 数组赋值的行为不同。我觉得我可能对 Python 有一些基本的误解。我最开始也在想,可能有性能方面的原因,让人更倾向于使用 itertools 而不是列表推导式或创建另一个 n 个元素的列表。

5 个回答

1

试试这个:

l = len(x)
x[::2] = itertools.repeat(False, l/2 if l % 2 == 0 else (l/2)+1)

你最开始的解决方案会陷入无限循环,这是因为repeat的设计就是这样的,具体可以参考文档

创建一个迭代器,可以不断返回同一个对象。除非你指定了次数参数,否则它会一直运行下去。

3

你在这段代码中想做的事情可能和你想的不太一样(我猜的)。比如说:
x[::2] 这个写法会返回一个包含 x 中每个 奇数 元素的切片,因为 x 的大小是 20,
所以这个切片的大小会是 10,但你却试图把一个大小为 1 的非可迭代对象赋值给它。

要成功使用你写的代码,你需要这样做:

x = [True]*20
x[::2] = [False]*10

这样就会把一个大小为 10 的可迭代对象赋值给一个大小为 10 的切片。

为什么要在元素数量上摸黑呢?可以使用

len(x[::2])  

这将等于 10,然后再使用

x[::2] = [False]*len(x[::2])

你也可以做类似这样的事情:

x = [True if (index & 0x1 == 0) else False for index, element in enumerate(x)]

编辑:由于提问者进行了编辑

关于 cycle 的文档 说它 会无限重复。 这意味着它会不断地“循环”遍历它所给定的迭代器。

Repeat 的实现类似,但 文档说明它
会无限运行,除非指定 times 参数。
而在提问者的代码中并没有这样做。因此这两者都会导致无限循环。

关于 itertools 更快的评论。是的,itertools 通常比其他实现要快,因为它们经过优化,尽可能地提高了速度。

不过,如果你不想重新创建一个列表,可以使用 生成器表达式,比如下面这个:

x = (True if (index & 0x1 == 0) else False for index, element in enumerate(x))

生成器表达式不会把所有元素都存储在内存中,而是根据需要生成它们,但生成器函数是可以用完的。

例如:

x = [True]*20
print(x)
y = (True if (index & 0x1 == 0) else False for index, element in enumerate(x))
print ([a for a in y])
print ([a for a in y])

会先打印 x,然后打印生成器 y 中的元素,最后打印一个空列表,因为生成器已经用完了。

2

正如Mark Tolonen在评论中简洁地指出的,你在使用itertools时出现无限循环的原因是,Python在进行列表赋值时,会检查右边的内容长度。

现在我们来深入了解一下……

当你写:

x[::2] = itertools.repeat(False)

左边的部分(x[::2])是一个列表,而你正在给这个列表赋值,赋的值是itertools.repeat(False)这个可迭代对象。由于没有给它指定长度,所以它会一直循环下去(具体可以参考文档)。

如果你深入查看cPython的列表赋值代码,你会发现一个名字很不幸的函数list_ass_slice,这个函数是很多列表赋值操作的根源。在这段代码中,你会看到这一部分

v_as_SF = PySequence_Fast(v, "can only assign an iterable");
if(v_as_SF == NULL)
    goto Error;
n = PySequence_Fast_GET_SIZE(v_as_SF);

这里它试图获取你要赋值给列表的可迭代对象的长度(n)。但是,在到达这一点之前,它就卡在了PySequence_Fast,在这里它试图将你的可迭代对象转换为一个列表(使用PySequence_List),最终它创建了一个空列表,并试图用你的可迭代对象来扩展它。

为了用可迭代对象扩展列表,它使用了listextend(),在这里你会看到问题的根源:

/* Run iterator to exhaustion. */
for (;;) {

就这样。

或者我认为是这样…… :) 这是个有趣的问题,所以我想玩一下,深入源码看看发生了什么,结果就找到了这里。

至于numpy数组的不同表现,那只是因为numpy.array的赋值处理方式不同。

注意,使用itertools.repeat在numpy中是无效的,但它不会卡住(我没有检查实现来弄清楚为什么):

>>> import numpy, itertools
>>> x = numpy.ones(10,dtype='bool')
>>> x[::2] = itertools.repeat(False)
>>> x
array([ True,  True,  True,  True,  True,  True,  True,  True,  True,  True], dtype=bool)
>>> #but the scalar assignment does work as advertised...
>>> x = numpy.ones(10,dtype='bool')
>>> x[::2] = False
>>> x
array([False,  True, False,  True, False,  True, False,  True, False,  True], dtype=bool)

撰写回答