题 Python 3生成器理解生成包括last的块


如果你在Python 3.7中有一个列表:

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

您可以将其转换为每个长度的块列表 n 使用两种常见的Python习语之一:

>>> n=3
>>> list(zip(*[iter(li)]*n))
[(0, 1, 2), (3, 4, 5), (6, 7, 8)]

从那以后就丢掉了最后一个不完整的元组 (9,10) 不长 n

你也可以这样做:

>>> [li[i:i+n] for i in range(0,len(li),n)]
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]

如果你想要最后一个子列表,即使它少于 n 元素。

假设我现在有一台发电机, gen,未知的长度或终止(所以打电话 list(gen)) 要么 sum(1 for _ in gen) 我想要的每一块都不是明智的。

我能够想出的最好的生成器表达式是这样的:

from itertools import zip_longest
sentinel=object()             # for use in filtering out ending chunks
gen=(e for e in range(22))    # fill in for the actual gen

g3=(t if sentinel not in t else tuple(filter(lambda x: x != sentinel, t)) for t in zip_longest(*[iter(gen)]*n,fillvalue=sentinel))

这适用于预期目的:

>>> next(g3)
(0, 1, 2)
>>> next(g3)
(3, 4, 5)
>>> list(g3)
[(6, 7, 8), (9, 10)]

看起来很笨拙。我试过了:

  1. 运用 islice 但缺乏长度似乎难以克服;
  2. 使用哨兵 iter 但是哨兵版 iter 需要一个可调用的,而不是可迭代的。

有没有更惯用的 Python 3 用于长度为大的发电机的技术 n 包括可能小于的最后一个夹头 n

我也对生成器功能持开放态度。我正在寻找一些惯用的东西,而且更具可读性。


更新:

我认为DSM在他删除的答案中的方法非常好:

>>> g3=(iter(lambda it=iter(gen): tuple(islice(it, n)), ()))
>>> next(g3)
(0, 1, 2)
>>> list(g3)
[(3, 4, 5), (6, 7, 8), (9, 10)]

我对这个问题持开放态度 DUP 但是这个相关问题差不多已有10年了,并且专注于一个清单。没有  Python 3中的方法与生成器,你不知道长度,一次不想要一个块?


11
2017-07-20 15:56


起源


可能我误解了,但是有什么问题 islice 喜欢 for item in gen: print(tuple(islice(gen,3))) (更换 print同 yield 当然是为了发电机功能) - Chris_Rands
可能重复 stackoverflow.com/questions/434287/... , stackoverflow.com/questions/312443/..., stackoverflow.com/questions/8991506/... - Kasrâmvd
@Kasramvd:啊,是的 - 我的答案是肯定的 senderle的 用一行的默认值吧。 - DSM
@Kasramvd:我不认为那些是非常重复的,因为1)主要与已经在内存中的列表有关或2)没有采用Python 3.6+的新功能和3)有我列出的两个习语的一些变体。相关问题已有10年历史。我们是否认为没有新的Python 3只能这样做? - dawg


答案:


我认为只要你想把它装进一个班轮里,这总会很乱。 我会咬紧牙关,在这里使用发电机功能。如果您不知道实际尺寸(例如,如果 gen 是一个无限的发电机等)。

from itertools import islice

def chunk(gen, k):
    """Efficiently split `gen` into chunks of size `k`.

       Args:
           gen: Iterator to chunk.
           k: Number of elements per chunk.

       Yields:
           Chunks as a list.
    """ 
    while True:
        chunk = [*islice(gen, 0, k)]
        if chunk:
            yield chunk
        else:
            break

>>> gen = iter(list(range(11)))
>>> list(chunk(gen))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]

有人可能会有更好的建议,但这就是我的方法。


8
2017-07-20 16:09



这既清晰又快速。 (我对它们进行了基准测试......)谢谢 - dawg
见时间;-) - dawg
你也可以这样做 chunk = (*islice(it, 0, k),) 如果你想要元组列表与列表列表。在Python <3.5, tuple(islice(it, 0, k)) - dawg


这感觉就像一个非常合理的方法,只建立在itertools上。

>>> g = (i for i in range(10))
>>> g3 = takewhile(lambda x: x, (list(islice(g,3)) for _ in count(0)))
>>> list(g3)
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

3
2017-07-20 16:16





我在这里总结了一些答案。

我最初编写它的方式实际上是Python 3.7上最快的。对于一个班轮,这可能是最好的。

冷速的修改版本 回答 是快速和Pythonic和可读。

其他答案都是类似的速度。

基准:

from __future__ import print_function

try:
    from itertools import zip_longest, takewhile, islice, count 
except ImportError:
    from itertools import takewhile, islice, count  
    from itertools import izip_longest as zip_longest
from collections import deque 

def f1(it,k):
    sentinel=object()
    for t in (t if sentinel not in t else tuple(filter(lambda x: x != sentinel, t)) for t in zip_longest(*[iter(it)]*k, fillvalue=sentinel)):
        yield t

def f2(it,k): 
    for t in (iter(lambda it=iter(it): tuple(islice(it, k)), ())):
        yield t

def f3(it,k):
    while True:
        chunk = (*islice(it, 0, k),)   # tuple(islice(it, 0, k)) if Python < 3.5
        if chunk:
            yield chunk
        else:
            break

def f4(it,k):
    for t in takewhile(lambda x: x, (tuple(islice(it,k)) for _ in count(0))):
        yield t

if __name__=='__main__':
    import timeit    
    def tf(f, k, x):
        data=(y for y in range(x))
        return deque(f(data, k), maxlen=3)

    k=3
    for f in (f1,f2,f3,f4):
        print(f.__name__, tf(f,k,100000))
    for case, x in (('small',10000),('med',100000),('large',1000000)):  
        print("Case {}, {:,} x {}".format(case,x,k))
        for f in (f1,f2,f3,f4):
            print("   {:^10s}{:.4f} secs".format(f.__name__, timeit.timeit("tf(f, k, x)", setup="from __main__ import f, tf, x, k", number=10)))    

结果如下:

f1 deque([(99993, 99994, 99995), (99996, 99997, 99998), (99999,)], maxlen=3)
f2 deque([(99993, 99994, 99995), (99996, 99997, 99998), (99999,)], maxlen=3)
f3 deque([(99993, 99994, 99995), (99996, 99997, 99998), (99999,)], maxlen=3)
f4 deque([(99993, 99994, 99995), (99996, 99997, 99998), (99999,)], maxlen=3)
Case small, 10,000 x 3
       f1    0.0125 secs
       f2    0.0231 secs
       f3    0.0185 secs
       f4    0.0250 secs
Case med, 100,000 x 3
       f1    0.1239 secs
       f2    0.2270 secs
       f3    0.1845 secs
       f4    0.2477 secs
Case large, 1,000,000 x 3
       f1    1.2140 secs
       f2    2.2431 secs
       f3    1.7967 secs
       f4    2.4697 secs

2
2017-07-21 17:47





具有生成器功能的此解决方案相当明确且简短:

def g3(seq):
    it = iter(seq)
    while True:
        head = list(itertools.islice(it, 3))
        if head:
            yield head
        else:
            break

1
2017-07-20 16:09





itertools recipe 该部分文件提供了各种发电机助手。

在这里你可以修改 take 与第二种形式 iter 创建一个块生成器。

from itertools import islice

def chunks(n, it):
    it = iter(it)
    return iter(lambda: tuple(islice(it, n)), ())

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

print(*chunks(3, li))

产量

(0, 1, 2) (3, 4, 5) (6, 7, 8) (9, 10)

1
2017-07-20 16:22





more_itertools.chunked

list(more_itertools.chunked(range(11), 3))
# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]

另见 资源

iter(functools.partial(more_itertools.take, n, iter(iterable)), [])

1
2017-07-30 16:34





我尝试使用 groupby 和 cycle。同 cycle 您可以选择一种模式如何对元素进行分组,因此它具有多种用途:

from itertools import groupby, cycle

gen=(e for e in range(11))
d = [list(g) for d, g in groupby(gen, key=lambda v, c=cycle('000111'): next(c))]
print([v for v in d])

输出:

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

0
2017-07-20 16:20