【问题标题】:Python 3 generator comprehension to generate chunks including lastPython 3 生成器理解生成块,包括最后
【发布时间】:2018-12-28 23:44:36
【问题描述】:

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

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

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

>>> 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 中没有带有生成器的 new 方法,您不知道长度并且一次只想要一个块?

【问题讨论】:

  • 可能我理解错了,但是islice 有什么问题,比如for item in gen: print(tuple(islice(gen,3)))(当然,将print 替换为yield 作为生成器函数)
  • @Kasramvd:啊,是的——我的答案只是senderle's,默认值是一行。
  • @Kasramvd:我不认为这些是完全重复的,因为 1)主要与内存中已经存在的列表有关,或者 2)不采用 Python 3.6+ 的新功能和 3)有一些变体我列出的两个成语。链接的问题是 10 岁。我们是否断定没有新的 Python 3 唯一方法可以做到这一点?

标签: python python-3.x generator


【解决方案1】:

我认为,只要您尝试将其装入一个衬里,这总是会很混乱。 我会硬着头皮在这里使用生成器功能。如果您不知道实际大小(例如,如果 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]]

可能有人有更好的建议,但我就是这样做的。

【讨论】:

  • 这既清晰又快速。 (我都对它们进行了基准测试......)谢谢
  • 查看时间 ;-)
  • 如果您想要元组列表与列表列表,您也可以使用chunk = (*islice(it, 0, k),)。在 Python tuple(islice(it, 0, k))
【解决方案2】:

这感觉像是一个非常合理的方法,它只建立在 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】:

    我在这里整理了一些答案的时间。

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

    cold speed 的 answer 的修改版本既快速又 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
    

    【讨论】:

      【解决方案4】:

      这个带有生成器函数的解决方案相当明确且简短:

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

      【讨论】:

        【解决方案5】:

        文档的itertools recipe 部分提供了各种生成器助手。

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

        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)
        

        【讨论】:

          【解决方案6】:

          more_itertools.chunked:

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

          另见source

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

          【讨论】:

            【解决方案7】:

            我尝试使用groupbycycle。使用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]]
            

            【讨论】:

              【解决方案8】:

              我们可以通过使用 itertools 文档页面中给出的 grouper 函数来做到这一点。

              from itertools import zip_longest
              
              def grouper(iterable, n, fillvalue=None):
                  "Collect data into fixed-length chunks or blocks"
                  # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
                  args = [iter(iterable)] * n
                  return zip_longest(fillvalue=fillvalue, *args)
              
              def out_iterator(lst):
                  for each in grouper(lst,n):
                      if None in each:
                          yield each[:each.index(None)]
                      else:
                          yield each
              a=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
              n=3
              print(list(out_iterator(a)))
              

              输出:

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2011-12-19
                • 1970-01-01
                • 2018-09-30
                • 2017-10-04
                • 2022-01-17
                • 2016-10-04
                • 2017-12-27
                • 1970-01-01
                相关资源
                最近更新 更多