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
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:
创建一个迭代器,从迭代中返回所选元素
在开始之前,需要明确的是,选择切片方法的正确顺序通常是:
\nallbutone = list(someiterable)[1:]
。这比任何其他方法更简单,并且在大多数情况下通常更快。list
(因此内存可能是一个问题)itertools.islice
通常是正确的解决方案,因为它足够简单,并且性能成本通常并不重要。islice
性能慢得令人无法接受(它增加了生成每个项目的一些开销,尽管不可否认它的数量相当小)并且要跳过的数据量很小,而要跳过的数据量很小。包含的内容非常庞大(例如,OP的场景是跳过单个元素并保留其余元素),请继续阅读如果您发现自己处于情况#3,那么您就处于这样一种情况islice
:(相对)快速地绕过初始元素的能力不足以弥补生产其余元素的增量成本。在这种情况下,您可以通过将问题从选择after 的所有元素反转n
为丢弃before 的 所有元素来提高性能n
。
对于这种方法,您手动将输入转换为迭代器,然后显式提取并丢弃n
值,然后迭代迭代器中剩下的内容(但没有每个元素的开销islice
)。例如,对于 的输入myinput = list(range(1, 10000))
,选择元素 1 到末尾的选项是:
# 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如果要丢弃的元素数量较大,最好从文档中借用该consume
itertools
配方:
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
元素:
# 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从性能角度来看,对于大多数大型输入来说,这都具有显着优势(例外:range
Python 3 本身已经针对普通切片进行了优化;普通切片在实际对象上无法击败range
)。ipython3
微基准测试(在 CPython 3.6、64 位 Linux 构建上)说明了这一点(slurp
设置中的定义只是一种以最低开销方法运行可迭代对象的方法,因此我们可以最大限度地减少我们不感兴趣的内容的影响):
>>> 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
是表现最差的(少量),普通切片稍微好一些(这强化了我的观点,即当你有实际序列时,普通切片几乎总是最好的解决方案),并且“转换为迭代器,丢弃初始值,使用剩余部分”相对而言,该方法以巨大的优势获胜(远低于任一解决方案的一半时间)。
对于微小的输入,这种好处不会显现出来,因为加载/调用iter
/的固定开销next
,尤其是islice
,将超过节省的费用:
>>> %%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 效率低下),并且在“跳过很多,保留一些”的情况下,它会做得更糟(相对于急切切片)。islice
islice
n
next
islice
O(1)
(list
、tuple
、str
等,不包括collections.deque
)将其埋在底部,因为虽然它绝对是最快的解决方案,但它也是特定于类型的(不适用于任意迭代)并且它依赖于实现细节(具体来说,酸洗的实现) Python 内置序列的功能;这不太可能改变,因为如果删除支持,它会破坏现有的腌制数据,但这不是语言保证)。如果您处于以下情况:
\nlist
(或其他带有索引的内置平面序列类型,O(1)
例如tuple
or str
,但不是 collections.deque
,它是O(n)
索引)你可以通过直接操作迭代器来跳过有成本的项目来做一件可怕的事情O(1)
(其中使用consume
配方,无论内联与否,都O(n)
在跳过的项目中)。它本质上与上面的方法#3相同,除了我们滥用序列迭代器的设计可以向前跳到我们关心的索引:
# 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
)、普通切片(具有相关的内存成本和急切操作)以及手动更改迭代器位置的时间:
>>> 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
从预高级迭代器中提取项目。
collections.deque
对于任意可迭代对象,这显然不起作用(dict
[及其视图和迭代器]、set
[及其迭代器]、打开文件对象等),因此内联consume
仍然是唯一真正的选择。collections.deque
不过这是一种特殊情况,因为虽然它不支持切片,并且它的迭代器不支持__setstate__
,但它确实支持旋转,因此您可以编写一个自定义包装器来将所需的元素旋转到前面,islice
然后将其关闭,然后将其旋转回来,切片就完成了(依赖于在迭代过程中不需要修改deque
)。例如:
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 块指针的工作复杂程度与islice
ing 一样)。
归档时间: |
|
查看次数: |
9393 次 |
最近记录: |