迭代列表切片

40

我希望有一个可以迭代列表切片的算法。切片的大小在函数外部设定并且可能不同。

在我的想象中,它应该是这样的:

for list_of_x_items in fatherList:
    foo(list_of_x_items)
有没有一种方法可以正确地定义list_of_x_items或其他使用Python 2.5实现的方式? edit1: Clarification,“分区”和“滑动窗口”这两个术语似乎都适用于我的任务,但我不是专家。因此,我将更深入地解释问题并添加到问题中:
fatherList是我从文件中获取的多级numpy.array。函数必须找到系列的平均值(用户提供系列的长度)。为了进行平均化,我使用mean()函数。现在为了扩展问题: edit2:如何修改您提供的函数以存储额外的项目,并在下一个fatherList被馈送到函数时使用它们?
例如,如果列表长度为10且块的大小为3,则将列表的第10个成员存储并附加到下一个列表的开头。

相关:


1
你似乎在描述一个针对原始列表子集的操作。 "slices" 可能指连续范围的单个部分,但你正在寻找的操作通常称为 "partitioning"。你可能会发现这个链接有用:https://dev59.com/B3M_5IYBdhLWcg3w2XLw - jmanning2k
我实际上在考虑对一个序列进行宽度为n的“滑动窗口”操作,但是看了Nadia的答案后,发现还有另一种解释这个问题的方式,可能更接近于“分区”。也许原帖作者会澄清这一点。 - ThomasH
10个回答

74

如果您想将列表切片,可以使用以下技巧:

list_of_slices = zip(*(iter(the_list),) * slice_size)
例如。
>>> zip(*(iter(range(10)),) * 3)
[(0, 1, 2), (3, 4, 5), (6, 7, 8)]

如果列表的项数不能被切片大小整除,并且您想使用None填充列表,您可以执行以下操作:

>>> map(None, *(iter(range(10)),) * 3)
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)]

这是一个不太光彩的小技巧。


好的,我来解释一下它是如何工作的。虽然解释起来可能有点棘手,但我会尽力的。

首先讲一下背景:

在Python中,你可以通过以下方式将一个列表乘以一个数字:

[1, 2, 3] * 3 -> [1, 2, 3, 1, 2, 3, 1, 2, 3]
([1, 2, 3],) * 3 -> ([1, 2, 3], [1, 2, 3], [1, 2, 3])

迭代器对象可以像这样被消耗掉一次:

And an iterator object can be consumed once like this:

>>> l=iter([1, 2, 3])
>>> l.next()
1
>>> l.next()
2
>>> l.next()
3

zip函数返回一个元组列表,第i个元组包含每个参数序列或可迭代对象中第i个元素。例如:

zip([1, 2, 3], [20, 30, 40]) -> [(1, 20), (2, 30), (3, 40)]
zip(*[(1, 20), (2, 30), (3, 40)]) -> [[1, 2, 3], [20, 30, 40]]

在zip前面的星号用于解包参数。你可以在这里找到更多详细信息。

zip(*[(1, 20), (2, 30), (3, 40)])

实际上等同于

zip((1, 20), (2, 30), (3, 40))

但是可以使用可变数量的参数。

现在回到这个技巧:

list_of_slices = zip(*(iter(the_list),) * slice_size)

iter(the_list) -> 将列表转换为迭代器。

(iter(the_list),) * N -> 会生成 N 个对 the_list 迭代器的引用。

zip(*(iter(the_list),) * N) -> 将这些迭代器列表传递给 zip 函数,zip 函数将把它们分组成大小为 N 的元组。但是由于所有 N 个项实际上都是对同一个迭代器 iter(the_list) 的引用,所以结果将重复调用原始迭代器上的 next()

希望这能解释清楚。我建议您选择更易理解的解决方案。我只是想提及这个技巧,因为我喜欢它。


4
有趣的是,您正在默默地利用“*”解包运算符的低优先级,这使得有趣的结构如“f(LN)”成为可能,而我在任何文档中都找不到关于此的说明。 - ThomasH
2
@ThomasH:f(*args)是Python中的扩展调用语法。您可以将任何可迭代的内容放在星号(*)的右侧,例如f(*[1]+g()) - jfs
@J.F.Sebastian 是的,谢谢。让我惊讶的是这个操作符的优先级。你的例子解析为 f(*([1]+g())) - ThomasH
有点像 Python 的风格,我很喜欢。 - lkostka

22
如果您想要能够使用任何可迭代对象,您可以使用这些函数:
from itertools import chain, islice

def ichunked(seq, chunksize):
    """Yields items from an iterator in iterable chunks."""
    it = iter(seq)
    while True:
        yield chain([it.next()], islice(it, chunksize-1))

def chunked(seq, chunksize):
    """Yields items from an iterator in list chunks."""
    for chunk in ichunked(seq, chunksize):
        yield list(chunk)

1
非常漂亮。而且你可以通过添加参数来选择每个块的类型,使其更加智能:https://gist.github.com/1275417。 - Bite code
在Python 3中,使用next(it)而不是it.next()。对于那些想知道的人:islice不会引发StopIteration异常,这就是为什么我们要调用next的原因。 - Tim-Erwin
@Tim-Erwin 我在 Python 3.6 中将 next(it) 更改为 ichunked,但是我收到了警告 DeprecationWarning: generator 'ichunked' raised StopIteration。我不明白这是在哪里引发的,因为它不是显式的。 - davidA
1
@meowsqueak 这个错误是在 next() 中引发的。这在 Python 3.5 及以下版本中是预期的,在 3.6 中已弃用,在 3.7 中已损坏。我在答案中添加了一个与 3.7+ 兼容的版本。 - Tim-Erwin
1
@meowsqueak,其他人认为我的编辑没有用处并拒绝了它。你可以在下面找到它作为一个单独的答案。 - Tim-Erwin

12

使用生成器:

big_list = [1,2,3,4,5,6,7,8,9]
slice_length = 3
def sliceIterator(lst, sliceLen):
    for i in range(len(lst) - sliceLen + 1):
        yield lst[i:i + sliceLen]

for slice in sliceIterator(big_list, slice_length):
    foo(slice)

sliceIterator 实现了一个宽度为 sliceLen 的“滑动窗口”在序列 lst 上,即它产生重叠的切片:[1,2,3]、[2,3,4]、[3,4,5]... 不确定这是否是OP的意图。


9
你的意思是这样的吗:
def callonslices(size, fatherList, foo):
  for i in xrange(0, len(fatherList), size):
    foo(fatherList[i:i+size])

如果这大致是您想要的功能,您可以使用生成器稍微优化一下:

def sliceup(size, fatherList):
  for i in xrange(0, len(fatherList), size):
    yield fatherList[i:i+size]

然后:

def callonslices(size, fatherList, foo):
  for sli in sliceup(size, fatherList):
    foo(sli)

我认为你会得到不规则大小的列表。无法确定OP想要什么,但这可能更接近:for i in xrange(0, len(fatherList)-size, 1): - hughdbrown
更正:对于i在xrange(len(fatherList)- size + 1)中: - hughdbrown
“不规则大小”?它们的长度都是 size,除了最后一个可能会有所不同——且不重叠。当长度为6且大小为7时,请思考您的代码会发生什么......有5个不同长度的切片...... - Alex Martelli
更好了;-),但是使用长度为6和大小为8仍然存在同样的问题。 - Alex Martelli
我一直在寻找一个好用的东西,因为它看起来是一个非常普遍的问题。我的笔记:实现简单易懂,但不能与迭代器一起使用... :( 不是 len(list(fatherList))。 - lukmdo

8

回答问题的最后一部分:

问题更新:如何修改您提供的函数以存储额外项目,并在下一个fatherList被输入到函数时使用它们?

如果需要存储状态,可以使用对象来实现。

class Chunker(object):
    """Split `iterable` on evenly sized chunks.

    Leftovers are remembered and yielded at the next call.
    """
    def __init__(self, chunksize):
        assert chunksize > 0
        self.chunksize = chunksize        
        self.chunk = []

    def __call__(self, iterable):
        """Yield items from `iterable` `self.chunksize` at the time."""
        assert len(self.chunk) < self.chunksize
        for item in iterable:
            self.chunk.append(item)
            if len(self.chunk) == self.chunksize:
                # yield collected full chunk
                yield self.chunk
                self.chunk = [] 

例子:

chunker = Chunker(3)
for s in "abcd", "efgh":
    for chunk in chunker(s):
        print ''.join(chunk)

if chunker.chunk: # is there anything left?
    print ''.join(chunker.chunk)

输出:

abc
def
gh

在 for 循环之后,还需要另一个 yield self.chunk 来处理不是完整块大小的块。否则,这对任意列表大小都不起作用。 - aryeh
@aryeh: __call__ 可能会被调用多次,因此您不能在 for 循环后仅仅产生剩余项。这就是类的意义所在。它回答了问题中明确说明的部分:“在下一个 fatherList 被提供给函数时存储额外的项目并使用它们”。 - jfs
1
我的错误,我没有仔细阅读问题/示例。我以为这只是为了分组。我个人不喜欢最后的额外检查,并且会投票添加一个选项到__call__中,无论是保存额外的项目还是在结束时刷新它们,这样您就不需要可能在后面的if语句中重复for循环的主体。 - aryeh

6
我不确定,但似乎你想做的是所谓的移动平均。numpy提供了这方面的工具(卷积函数)。
>>> x = numpy.array(range(20)) >>> x array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) >>> n = 2 # 移动平均窗口大小 >>> numpy.convolve(numpy.ones(n)/n, x)[n-1:-n+1] array([ 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5, 11.5, 12.5, 13.5, 14.5, 15.5, 16.5, 17.5, 18.5])
很好的一点是它可以很好地适应不同的加权方案(只需将numpy.ones(n) / n更改为其他内容)。
您可以在此处找到完整的材料: http://www.scipy.org/Cookbook/SignalSmooth

3

扩展@Ants Aasma的答案:在Python 3.7中,处理StopIteration异常的方式发生了变化(根据PEP-479)。兼容的版本为:

from itertools import chain, islice

def ichunked(seq, chunksize):
    it = iter(seq)
    while True:
        try:
            yield chain([next(it)], islice(it, chunksize - 1))
        except StopIteration:
            return

2

你的问题需要更多细节,但是如下回答可能会有所帮助:

def iterate_over_slices(the_list, slice_size):
    for start in range(0, len(the_list)-slice_size):
        slice = the_list[start:start+slice_size]
        foo(slice)

2
在导入 itertools 后,为了处理不可分块但没有填充的大小,可以使用类似 Nadia 答案的近乎一行的代码:
>>> import itertools as itt
>>> chunksize = 5
>>> myseq = range(18)
>>> cnt = itt.count()
>>> print [ tuple(grp) for k,grp in itt.groupby(myseq, key=lambda x: cnt.next()//chunksize%2)]
[(0, 1, 2, 3, 4), (5, 6, 7, 8, 9), (10, 11, 12, 13, 14), (15, 16, 17)]

如果你愿意的话,可以使用enumerate()来替代itertools.count()的要求,但是代码会变得更加丑陋:
[ [e[1] for e in grp] for k,grp in itt.groupby(enumerate(myseq), key=lambda x: x[0]//chunksize%2) ]

在这个例子中,enumerate() 是多余的,但不是所有的序列都像这样整齐,显然。
与其他答案相比,这个方法并不那么简洁,但在紧急情况下很有用,特别是如果已经导入了itertools

0
一个将列表或迭代器切片成给定大小块的函数。如果最后一块较小,也会正确处理该情况:
def slice_iterator(data, slice_len):
    it = iter(data)
    while True:
        items = []
        for index in range(slice_len):
            try:
                item = next(it)
            except StopIteration:
                if items == []:
                    return # we are done
                else:
                    break # exits the "for" loop
            items.append(item)
        yield items

使用示例:

for slice in slice_iterator([1,2,3,4,5,6,7,8,9,10],3):
    print(slice)

结果:

[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10]

在某些情况下,这个解决方案可能会比其他一些解决方案使用更多的代码行,并且速度可能会稍慢。但是,如果使用islice,这种情况可以得到改善。 - mit

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