Python:遍历子列表

Cha*_*l72 17 python generator slice python-3.x

通常,当您想在Python中迭代列表的一部分时,最简单的方法就是对列表进行切片.

# Iterate over everything except the first item in a list
#
items = [1,2,3,4]
iterrange = (x for x in items[1:])
Run Code Online (Sandbox Code Playgroud)

但切片运算符会创建一个新列表,在许多情况下甚至不需要这样做.理想情况下,我想要一种创建生成器的切片函数,而不是新的列表对象.可以通过创建一个生成器表达式来实现与此类似的东西,该表达式使用a range来仅返回列表的某些部分:

# Create a generator expression that returns everything except 
# the first item in the list
#
iterrange = (x for x, idx in zip(items, range(0, len(items))) if idx != 0)
Run Code Online (Sandbox Code Playgroud)

但这有点麻烦.我想知道是否有更好,更优雅的方式来做到这一点.那么,切片列表的最简单方法是什么,以便创建生成器表达式而不是新的列表对象?

Seb*_*ann 20

使用itertools.islice:

import itertools

l = range(20)

for i in itertools.islice(l,10,15):
    print i

10
11
12
13
14
Run Code Online (Sandbox Code Playgroud)

来自doc:

创建一个迭代器,从迭代中返回所选元素

  • @kojiro,不确定你的意思.`islice`对象是[在c]中实现的(http://hg.python.org/cpython/file/45cd2d816f4d/Modules/itertoolsmodule.c#l1103)在cpython中. (3认同)
  • @kojiro这不是真的.我执行了一个快速的timeit测试,显示使用生成器表达式确实比重新创建列表更快.如果你评论第二个陈述,我真的不明白你的观点.`isclice`正是OP所寻求的.获得有限生成器而不是新列表的快速而干净的方法.问题中显示的方式与OP已经指出的良好编码风格相去甚远.我认为这只是被理解为OP希望实现的一个例子. (3认同)

Sha*_*ger 5

在开始之前,需要明确的是,选择切片方法的正确顺序通常是:

\n
    \n
  1. 使用常规切片(复制除最长输入之外的所有输入的成本通常没有意义,并且代码要简单得多)。如果输入可能不是可切片序列类型,请将其转换为一,然后切片,例如allbutone = list(someiterable)[1:]。这比任何其他方法更简单,并且在大多数情况下通常更快。
  2. \n
  3. 如果常规切片不可行(不能保证输入是一个序列,并且在切片之前转换为序列可能会导致内存问题,或者它很大并且切片覆盖了其中的大部分,例如跳过第一个) 10M 元素的 1000 个和最后 1000 个元素list(因此内存可能是一个问题)itertools.islice通常是正确的解决方案,因为它足够简单,并且性能成本通常并不重要。
  4. \n
  5. 当且仅当,islice性能慢得令人无法接受(它增加了生成每个项目的一些开销,尽管不可否认它的数量相当小)并且要跳过的数据量很小,而要跳过的数据量很小。包含的内容非常庞大(例如,OP的场景是跳过单个元素并保留其余元素),请继续阅读
  6. \n
\n

如果您发现自己处于情况#3,那么您就处于这样一种情况islice:(相对)快速地绕过初始元素的能力不足以弥补生产其余元素的增量成本。在这种情况下,您可以通过将问题从选择after 的所有元素反转n丢弃before 的 所有元素来提高性能n

\n

对于这种方法,您手动将输入转换为迭代器,然后显式提取并丢弃n值,然后迭代迭代器中剩下的内容(但没有每个元素的开销islice)。例如,对于 的输入myinput = list(range(1, 10000)),选择元素 1 到末尾的选项是:

\n
# Approach 1, OP\'s approach, simple slice:\nfor x in myinput[1:]:\n\n# Approach 2, Sebastian\'s approach, using itertools.islice:\nfor x in islice(myinput, 1, None):\n\n# Approach 3 (my approach)\nmyiter = iter(myinput)  # Explicitly create iterator from input (looping does this already)\nnext(myiter, None) # Throw away one element, providing None default to avoid StopIteration error\nfor x in myiter:  # Iterate unwrapped iterator\n
Run Code Online (Sandbox Code Playgroud)\n

如果要丢弃的元素数量较大,最好文档中借用consumeitertools配方:

\n
def consume(iterator, n=None):\n    "Advance the iterator n-steps ahead. If n is None, consume entirely."\n    # Use functions that consume iterators at C speed.\n    if n is None:\n        # feed the entire iterator into a zero-length deque\n        collections.deque(iterator, maxlen=0)\n    else:\n        # advance to the empty slice starting at position n\n        next(islice(iterator, n, n), None)\n
Run Code Online (Sandbox Code Playgroud)\n

这使得这些方法可以概括为跳过n元素:

\n
# Approach 1, OP\'s approach, simple slice:\nfor x in myinput[n:]:\n\n# Approach 2, Sebastian\'s approach, using itertools.islice:\nfor x in islice(myinput, n, None):\n\n# Approach 3 (my approach)\nmyiter = iter(myinput)  # Explicitly create iterator from input (looping does this already)\nconsume(myiter, n)      # Throw away n elements\n# Or inlined consume as next(islice(myiter, n, n), None)\nfor x in myiter:        # Iterate unwrapped iterator\n
Run Code Online (Sandbox Code Playgroud)\n

从性能角度来看,对于大多数大型输入来说,这都具有显着优势(例外:rangePython 3 本身已经针对普通切片进行了优化;普通切片在实际对象上无法击败range)。ipython3微基准测试(在 CPython 3.6、64 位 Linux 构建上)说明了这一点(slurp设置中的定义只是一种以最低开销方法运行可迭代对象的方法,因此我们可以最大限度地减少我们不感兴趣的内容的影响):

\n
>>> from itertools import islice\n>>> from collections import deque\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))\n... slurp(r[1:])\n...\n65.8 \xce\xbcs \xc2\xb1 109 ns per loop (mean \xc2\xb1 std. dev. of 5 runs, 10000 loops each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))\n... slurp(islice(r, 1, None))\n...\n70.7 \xce\xbcs \xc2\xb1 104 ns per loop (mean \xc2\xb1 std. dev. of 5 runs, 10000 loops each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))\n... ir = iter(r)\n... next(islice(ir, 1, 1), None)  # Inlined consume for simplicity, but with islice wrapping to show generalized usage\n... slurp(ir)\n...\n30.3 \xce\xbcs \xc2\xb1 64.1 ns per loop (mean \xc2\xb1 std. dev. of 5 runs, 10000 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

显然,我的解决方案的额外复杂性通常并不值得,但对于中等大小的输入(本例中为 10K 元素),性能优势是显而易见的;islice是表现最差的(少量),普通切片稍微好一些(这强化了我的观点,即当你有实际序列时,普通切片几乎总是最好的解决方案),并且“转换为迭代器,丢弃初始值,使用剩余部分”相对而言,该方法以巨大的优势获胜(远低于任一解决方案的一半时间)。

\n

对于微小的输入,这种好处不会显现出来,因为加载/调用iter/的固定开销next,尤其是islice,将超过节省的费用:

\n
>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))\n... slurp(r[1:])\n...\n207 ns \xc2\xb1 1.86 ns per loop (mean \xc2\xb1 std. dev. of 5 runs, 1000000 loops each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))\n... slurp(islice(r, 1, None))\n...\n307 ns \xc2\xb1 1.71 ns per loop (mean \xc2\xb1 std. dev. of 5 runs, 1000000 loops each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))\n... ir = iter(r)\n... next(islice(ir, 1, 1), None)  # Inlined consume for simplicity, but with islice wrapping to show generalized usage\n... slurp(ir)\n...\n518 ns \xc2\xb1 4.5 ns per loop (mean \xc2\xb1 std. dev. of 5 runs, 1000000 loops each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))\n... ir = iter(r)\n... next(ir, None)  # To show fixed overhead of islice, use next without it\n... slurp(ir)\n...\n341 ns \xc2\xb1 0.947 ns per loop (mean \xc2\xb1 std. dev. of 5 runs, 1000000 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

但正如您所看到的,即使对于 10 个元素,islice-free 方法也没有差多少;100 个元素时,islice-free 方法比所有竞争对手都快,而 200 个元素时,广义next+击败了所有竞争对手(考虑到 180 ns 的开销,islice显然它没有击败-free ,但这已经弥补了通过概括为将跳过元素作为一个步骤,而不是需要重复调​​用来跳过多个元素)。由于包装器严格执行每个元素的开销, Plain很少能在“跳过一些,保留很多”的情况下获胜(直到大约 100K 元素之前,它在微基准测试中并没有明显击败急切的切片;它的内存效率很高,但是CPU 效率低下),并且在“跳过很多,保留一些”的情况下,它会做得更糟(相对于急切切片)。isliceislicennextislice

\n
\n

当性能至关重要时,针对特定内置序列的特殊情况黑客攻击

\n

大多数带有索引的内置序列的特殊情况O(1)listtuplestr等,不包括collections.deque

\n

将其埋在底部,因为虽然它绝对是最快的解决方案,但它也是特定于类型的(不适用于任意迭代)并且它依赖于实现细节(具体来说,酸洗的实现) Python 内置序列的功能;这不太可能改变,因为如果删除支持,它会破坏现有的腌制数据,但这不是语言保证)。如果您处于以下情况:

\n
    \n
  1. 输入是 a list(或其他带有索引的内置平面序列类型,O(1)例如tupleor str,但不是 collections.deque,它是O(n)索引)
  2. \n
  3. 要跳过的项目数量巨大
  4. \n
  5. 要选择的项目数量很大(您甚至不想为浅复制切片所产生的指针支付内存成本)
  6. \n
\n

你可以通过直接操作迭代器来跳过有成本的项目来做一件可怕的事情O(1)(其中使用consume配方,无论内联与否,都O(n)在跳过的项目中)。它本质上与上面的方法#3相同,除了我们滥用序列迭代器的设计可以向前跳到我们关心的索引:

\n
# Approach 4 (my hacky, but performant, approach)\nmyiter = iter(myinput)  # Explicitly create iterator from input like before\nmyiter.__setstate__(n)  # Set n as the next index to iterate\nfor x in myiter:        # Iterate updated iterator\n
Run Code Online (Sandbox Code Playgroud)\n

使用 CPython 3.11.1 64 位 Linux 构建,比较之前针对较大输入的最佳解决方案(使用 inlined- consume)、普通切片(具有相关的内存成本和急切操作)以及手动更改迭代器位置的时间:

\n
>>> from itertools import islice\n>>> from collections import deque\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(100_000_000))  # *Much* bigger input\n... ir = iter(r)\n... next(islice(ir, 90_000_000, 90_000_000), None)  # *Much* bigger skip\n... slurp(ir)                                       # *Much* larger amount to consume\n...\n339 ms \xc2\xb1 3.1 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(100_000_000))\n... slurp(r[90_000_000:])\n...\n104 ms \xc2\xb1 648 \xce\xbcs per loop (mean \xc2\xb1 std. dev. of 7 runs, 10 loops each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(100_000_000))\n... ir = iter(r)\n... ir.__setstate__(90_000_000)\n... slurp(ir)\n...\n32.7 ms \xc2\xb1 278 \xce\xbcs per loop (mean \xc2\xb1 std. dev. of 7 runs, 10 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

对于这种“跳过 90M,取 10M”的场景,普通切片大约需要 \xe2\x85\x93 优化内联的时间consume,而手动迭代器操作又需要 \xe2\x85\x93 普通切片的时间(因为普通切片切片实际上必须执行 3 倍的迭代工作,一次从输入复制到切片副本,一次对其进行迭代,一次在释放切片时递减引用)。如果您不希望保留跳过阈值之后的所有项目,则切片可能是最好的解决方案,但您可以islice在此时换行以n从预高级迭代器中提取项目。

\n

特殊情况为collections.deque

\n

对于任意可迭代对象,这显然不起作用(dict[及其视图和迭代器]、set[及其迭代器]、打开文件对象等),因此内联consume仍然是唯一真正的选择。collections.deque不过这是一种特殊情况,因为虽然它不支持切片,并且它的迭代器不支持__setstate__,但它确实支持旋转,因此您可以编写一个自定义包装器来将所需的元素旋转到前面,islice然后将其关闭,然后将其旋转回来,切片就完成了(依赖于在迭代过程中不需要修改deque)。例如:

\n
def fast_islice_deque(deq, *slc):\n    try:\n        [stop] = slc  # Check for simple case, just islicing from beginning\n    except ValueError:\n        pass\n    else:\n        yield from islice(deq, stop)  # No need for rotation, just pull what we need\n\n    # We need to rotate, which requires some fix-ups to indices first\n    start, stop, step = slice(*slc).indices(len(deq))\n    stop -= start  # Rotate takes care of start\n    deq.rotate(-start)  # Move elements we care about to start with tiny amount of work\n    try:\n        yield from islice(deq, None, stop, step)\n    finally:\n        deq.rotate(start)  # Restore original ordering with tiny amount of work\n
Run Code Online (Sandbox Code Playgroud)\n

同样,64 位 Linux 上的 CPython 3.11.1 的计时:

\n
>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = deque(range(100_000_000))  # Same huge input, as deque\n... ir = iter(r)\n... next(islice(ir, 90_000_000, 90_000_000), None)\n... slurp(ir)\n...\n368 ms \xc2\xb1 2.06 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = deque(range(100_000_000))\n... slurp(fast_islice_deque(r, 90_000_000, None))\n...\n245 ms \xc2\xb1 5.34 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n
Run Code Online (Sandbox Code Playgroud)\n

或者比较在跳过后拉出较少数量的项目:

\n
>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = deque(range(100_000_000))  # Same huge input, as deque\n... slurp(islice(r, 90_000_000, 90_001_000))  # Need islice to bound selection anyway, so no pre-consume\n...\n331 ms \xc2\xb1 4.43 ms per loop (mean \xc2\xb1 std. dev. of 7 runs, 1 loop each)\n\n>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = deque(range(100_000_000))\n... slurp(fast_islice_deque(r, 90_000_000, 90_001_000))\n...\n19.4 ms \xc2\xb1 138 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

正如您所看到的,rotate在这两种情况下使用都可以节省大量工作,并且当您提取少量项目时它特别有用。当拉取大量且无限制的数字时,它并没有那么有用,仅仅是因为拉取 10M 个项目的成本明显高于跳过前 90M 个项目,并且您需要支付内联islice方法consume不需要的每个项目的开销将其用于您拉出的物品。但是,当提取较小/有限的数字时,两种方法都需要支付每个项目的islice开销成本,但是rotate基于 - 的解决方案虽然在技术上仍然如此O(n),但所做的工作却大大减少(它不涉及任何引用计数,只需修复up 块指针的工作复杂程度与isliceing 一样)。

\n