【问题标题】:Efficient iteration over slice in PythonPython中切片的高效迭代
【发布时间】:2013-08-05 14:26:08
【问题描述】:

Python 中的切片操作的迭代效率如何?如果切片是不可避免的,还有其他选择吗?

我知道对列表的切片操作是 O(k),其中 k 是切片的大小。

x[5 : 5+k]  # O(k) copy operation

但是,当迭代列表的一部分时,我发现最干净(也是最 Pythonic?)的方法(无需求助于索引)是:

for elem in x[5 : 5+k]:
  print elem

但是我的直觉是,这仍然会导致子列表的昂贵副本,而不是简单地迭代现有列表。

【问题讨论】:

  • 如果您担心复制切片,我想另一种方法是使用 range(5, 5 + k) 迭代索引。
  • 危险!你之前得到了不好的建议; itertools.islice 不像我们想象的那样工作。如果您使用从 1000000 开始的 islice,Python 将遍历列表的前 1000000 个元素,然后再产生任何内容。这可能会使线性时间算法变得二次或更糟。

标签: python performance iteration slice


【解决方案1】:

用途:

for elem in x[5 : 5+k]:

这是 Pythonic!在您分析您的代码并确定这是一个瓶颈之前,请不要更改它——尽管我怀疑您是否会发现这是瓶颈的主要来源。


就速度而言,它可能是您的最佳选择:

In [30]: x = range(100)

In [31]: k = 90

In [32]: %timeit x[5:5+k]
1000000 loops, best of 3: 357 ns per loop

In [35]: %timeit list(IT.islice(x, 5, 5+k))
100000 loops, best of 3: 2.42 us per loop

In [36]: %timeit [x[i] for i in xrange(5, 5+k)]
100000 loops, best of 3: 5.71 us per loop

就记忆力而言,它并没有你想象的那么糟糕。 x[5: 5+k]x 部分的副本。因此,即使x 中的对象很大,x[5: 5+k] 也会创建一个包含 k 个元素的新列表,这些元素引用了 x 中的 相同 对象。因此,您只需要额外的内存来创建一个包含 k 对预先存在的对象的引用的列表。这可能不会成为任何内存问题的根源。

【讨论】:

    【解决方案2】:

    您可以使用itertools.islice 从列表中获取切片迭代器:

    例子:

    >>> from itertools import islice
    >>> lis = range(20)
    >>> for x in islice(lis, 10, None, 1):
    ...     print x
    ...     
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    

    更新:

    正如@user2357112 所指出的,islice 的性能取决于切片的起点和可迭代的大小,普通切片在几乎所有情况下都会很快,应该是首选。以下是更多时间比较:

    对于巨大的列表,当切片的起点小于列表大小的一半时,islice 略快或等于普通切片,对于更大的索引,普通切片显然是赢家。

    >>> def func(lis, n):
            it = iter(lis)
            for x in islice(it, n, None, 1):pass
    ...     
    >>> def func1(lis, n):
            #it = iter(lis)
            for x in islice(lis, n, None, 1):pass
    ...     
    >>> def func2(lis, n):
            for x in lis[n:]:pass
    ...     
    >>> lis = range(10**6)
    
    >>> n = 100
    >>> %timeit func(lis, n)
    10 loops, best of 3: 62.1 ms per loop
    >>> %timeit func1(lis, n)
    1 loops, best of 3: 60.8 ms per loop
    >>> %timeit func2(lis, n)
    1 loops, best of 3: 82.8 ms per loop
    
    >>> n = 1000
    >>> %timeit func(lis, n)
    10 loops, best of 3: 64.4 ms per loop
    >>> %timeit func1(lis, n)
    1 loops, best of 3: 60.3 ms per loop
    >>> %timeit func2(lis, n)
    1 loops, best of 3: 85.8 ms per loop
    
    >>> n = 10**4
    >>> %timeit func(lis, n)
    10 loops, best of 3: 61.4 ms per loop
    >>> %timeit func1(lis, n)
    10 loops, best of 3: 61 ms per loop
    >>> %timeit func2(lis, n)
    1 loops, best of 3: 80.8 ms per loop
    
    
    >>> n = (10**6)/2
    >>> %timeit func(lis, n)
    10 loops, best of 3: 39.2 ms per loop
    >>> %timeit func1(lis, n)
    10 loops, best of 3: 39.6 ms per loop
    >>> %timeit func2(lis, n)
    10 loops, best of 3: 41.5 ms per loop
    
    >>> n = (10**6)-1000
    >>> %timeit func(lis, n)
    100 loops, best of 3: 18.9 ms per loop
    >>> %timeit func1(lis, n)
    100 loops, best of 3: 18.8 ms per loop
    >>> %timeit func2(lis, n)
    10000 loops, best of 3: 50.9 us per loop    #clear winner for large index
    >>> %timeit func1(lis, n)
    

    对于小型列表,几乎所有情况下,普通切片都比 islice 快。

    >>> lis = range(1000)
    >>> n = 100
    >>> %timeit func(lis, n)
    10000 loops, best of 3: 60.7 us per loop
    >>> %timeit func1(lis, n)
    10000 loops, best of 3: 59.6 us per loop
    >>> %timeit func2(lis, n)
    10000 loops, best of 3: 59.9 us per loop
    
    >>> n = 500
    >>> %timeit func(lis, n)
    10000 loops, best of 3: 38.4 us per loop
    >>> %timeit func1(lis, n)
    10000 loops, best of 3: 33.9 us per loop
    >>> %timeit func2(lis, n)
    10000 loops, best of 3: 26.6 us per loop
    
    >>> n = 900
    >>> %timeit func(lis, n)
    10000 loops, best of 3: 20.1 us per loop
    >>> %timeit func1(lis, n)
    10000 loops, best of 3: 17.2 us per loop
    >>> %timeit func2(lis, n)
    10000 loops, best of 3: 11.3 us per loop
    

    结论:

    去普通切片。

    【讨论】:

    • itertools.islice 不能这样工作!它是为切片任意迭代而构建的,它不会尝试使用__getitem__。如果您尝试使用从1000000 开始的isliceislice 将循环遍历列表中的第一个1000000 项目,然后再产生任何内容,从而彻底破坏您的性能。
    • @user2357112 我添加了一些时间比较,你能解释一下这怎么能是二次更糟?计时结果有利于islice
    • 如果您尝试从头到尾迭代列表的大量小切片,则二次时间将是;您的运行时间将是切片数量的二次方,而不是列表大小的线性。
    • lis 是什么,这是什么 Python 版本?我的计时结果和你的相反。
    • @user2357112 lisrange(10**6)。这是 IPython 外壳,py2.7.4。
    【解决方案3】:

    只需遍历所需的索引,无需为此创建新切片:

    for i in xrange(5, 5+k):
        print x[i]
    

    当然:它看起来不像 Python,但它比创建新切片更有效,因为不会浪费额外的内存。另一种方法是使用迭代器,如@AshwiniChaudhary 的回答所示。

    【讨论】:

    • 基于索引的方法的另一个问题是IndexError
    • 我相信对于这样一个简单的使用场景,一个更简单的答案是最好的。对于我们这些来自类 C 背景的人来说,并非所有东西都必须是花哨的和 Python 的,带有迭代器、切片和理解等,有时一个好的旧的基于索引的遍历就可以了
    • 这实际上是最慢的解决方案。您可以通过timeit 试用。迭代器和切片一点都不花哨。它们是基本的核心 Python 功能。
    • @user2357112 好吧,真是大开眼界!
    【解决方案4】:

    您已经在切片上进行了 O(n) 迭代。在大多数情况下,这将比实际创建切片更令人担忧,这完全发生在优化的 C 中。一旦你制作了切片,循环切片的时间是制作切片的两倍,即使你不要用它做任何事情:

    >>> timeit.timeit('l[50:100]', 'import collections; l=range(150)')
    0.46978958638010226
    >>> timeit.timeit('for x in slice: pass',
                      'import collections; l=range(150); slice=l[50:100]')
    1.2332711270150867
    

    您可以尝试使用xrange 迭代索引,但考虑到检索列表元素所需的时间,它比切片要慢。即使你跳过那部分,它仍然比不上切片:

    >>> timeit.timeit('for i in xrange(50, 100): x = l[i]', 'l = range(150)')
    4.3081963062022055
    >>> timeit.timeit('for i in xrange(50, 100): pass', 'l = range(150)')
    1.675838213385532
    

    不要为此使用itertools.islice它将从一开始就遍历您的列表,而不是使用__getitem__ 跳到您想要的值。下面是一些时序数据,显示了它的性能如何取决于切片的开始位置:

    >>> timeit.timeit('next(itertools.islice(l, 9, None))', 'import itertools; l = r
    ange(1000000)')
    0.5628290558478852
    >>> timeit.timeit('next(itertools.islice(l, 999, None))', 'import itertools; l =
     range(1000000)')
    6.885294697594759
    

    这是islice 输给常规切片:

    >>> timeit.timeit('for i in itertools.islice(l, 900, None): pass', 'import itert
    ools; l = range(1000)')
    8.979957560911316
    >>> timeit.timeit('for i in l[900:]: pass', 'import itertools; l = range(1000)')
    
    3.0318417204211983
    

    这是在 Python 2.7.5 上,以防任何更高版本添加特定于列表的优化。

    【讨论】:

      【解决方案5】:

      我认为更好的方法是使用类似 c 的迭代,如果 'k' 很大(因为一个大的 'k' - 比如 10000000000000 - 甚至可以让你等待大约 10 个小时才能在 pythonic for 循环中得到答案)

      这是我试图告诉你的:

      i = 5 ## which is the initial value
      f = 5 + k ## which will be the final index
      
      while i < f:
          print(x[i])
          i += 1
      

      对于我告诉过的那个大 k,我假设这个可以在 5 小时内完成(就像 Python 中的 for 循环一样,大约需要 10 小时),因为它需要从 5 到 10000000000005 一次! 每次使用 'xrange()' 的 'range()' 甚至切片本身(如上所述)都会使程序执行 20000000000000 次迭代,我认为这可能会导致更长的执行时间。 (据我所知,使用生成器方法将返回一个可迭代对象,该对象需要首先完全运行生成器,并且需要两次才能完成工作;一个用于生成器本身,另一个用于“for”循环)

      已编辑:

      在 python 3 中,生成器方法/对象不需要首先运行来为 for 循环生成可迭代对象

      【讨论】:

      • 这其实更慢!计时看看。
      【解决方案6】:

      接受的答案没有提供有效的解决方案。它按照n 的顺序排列,其中n 是切片起点。如果n 很大,那将是一个问题。 后续结论(“Go for normal slices”)也不理想,因为它使用了k顺序的额外空间进行复制。

      Python 为切片问题提供了一个非常优雅和高效的解决方案,称为生成器表达式,它可以尽可能优化:O(1) 空间和 O(k) 运行时间:

      (l[i] for i in range(n,n+k))
      

      它类似于列表推导,除了它是惰性的,您可以将它与其他迭代器工具(如 itertools 模块或过滤器)结合使用。请注意将表达式括起来的圆括号。

      【讨论】:

        【解决方案7】:

        对于遍历子数组(对于创建子数组,请参阅 unutbu 的答案),切片比最坏情况下的索引 (l[1:]) 快。

        10 items
        ========
        Slicing:  2.570001e-06 s
        Indexing: 3.269997e-06 s
        
        100 items
        =========
        Slicing:  6.820001e-06 s
        Indexing: 1.220000e-05 s
        
        1000 items
        ==========
        Slicing:  7.647000e-05 s
        Indexing: 1.482100e-04 s
        
        10000 items
        ===========
        Slicing:  2.876200e-04 s
        Indexing: 5.270000e-04 s
        
        100000 items
        ============
        Slicing:  3.763300e-03 s
        Indexing: 7.731050e-03 s
        
        1000000 items
        =============
        Slicing:  2.963523e-02 s
        Indexing: 4.921381e-02 s
        

        基准代码:

        def f_slice(l):
            for v in l[1:]:
                _x = v
        
        def f_index(l):
            for i in range(1, len(l)):
                _x = l[i]
        
        from time import perf_counter
        def func_time(func, l):
            start = perf_counter()
            func(l)
            return perf_counter()-start
        
        def bench(num_item):
            l = list(range(num_item))
            times = 10
            t_index = t_slice = 0
            for _ in range(times):
                t_slice += func_time(f_slice, l)
                t_index += func_time(f_index, l)
            print(f"Slicing:  {t_slice/times:e} s")
            print(f"Indexing: {t_index/times:e} s")
        
        for i in range(1, 7):
            s = f"{10**i} items"
            print(s)
            print('='*len(s))
            bench(10**i)
            print()
        

        【讨论】:

          猜你喜欢
          • 2021-07-01
          • 1970-01-01
          • 1970-01-01
          • 2023-01-08
          • 2018-09-12
          • 2019-07-23
          • 2010-11-23
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多