【发布时间】:2017-05-30 19:53:08
【问题描述】:
假设我需要从序列末尾seq 到给定项目x(包括)的第一次出现之间的切片。写seq[-1:seq.index(x)-1:-1] 的天真尝试会产生一个微妙的错误:
seq = 'abc'
seq[-1:seq.index('b')-1:-1] # 'cb' as expected
seq[-1:seq.index('a')-1:-1] # '' because -1 is interpreted as end of seq
有什么惯用的写法吗?
seq[seq.index(x):][::-1] 工作正常,但对于大序列可能效率低下,因为它会创建一个额外的副本。 (最后我确实需要一个序列,所以需要一个副本;我只是不想创建第二个副本。)
顺便说一句,这是一个非常容易引入的错误,它可以通过许多测试,并且任何静态分析器都无法检测到(除非它警告每个切片都带有负步骤)。
更新
似乎没有完美/惯用的解决方案。我同意它可能不像我想象的那样经常成为瓶颈,所以在大多数情况下我会使用[pos:][::-1]。当性能很重要时,我会使用普通的if 检查。但是,我会接受我觉得有趣的解决方案,即使它很难阅读;它可能在某些极少数情况下有用(我确实需要将整个内容放入表达式中,并且我不想定义新函数)。
另外,我尝试过计时。对于列表,即使它们短至 2 个项目,额外切片似乎总是会受到 2 倍的惩罚。对于字符串,结果极度不一致,以至于我无话可说:
import timeit
for n in (2, 5, 10, 100, 1000, 10000, 100000, 1000000):
c = list(range(n))
# c = 'x' * n
pos = n // 2 # pretend the item was found in the middle
exprs = 'c[pos:][::-1]', 'c[:pos:-1] if pos else c[::-1]'
results = [timeit.Timer(expr, globals=globals()).autorange() for expr in exprs]
times = [t/loops for loops, t in results]
print(n, times[0]/times[1])
列表的结果(额外切片/没有额外切片时间的比率):
2 2.667782437753884
5 2.2672817613246914
10 1.4275235266754878
100 1.6167102119737584
1000 1.7309116253903338
10000 3.606259720606781
100000 2.636049703318956
1000000 1.9915776615090277
当然,这忽略了这样一个事实,即无论我们对结果切片做什么,相对而言,当切片很短时,成本要高得多。尽管如此,我同意对于小尺寸的序列,[::-1] 通常非常好。
【问题讨论】:
-
seq[seq.index(x):][::-1](或reversed(seq[seq.index(x):]))是Pythonic方式,如premature optimization is the root of all evil -
@TemporalWolf 我大体上同意。但是,即使在第一次迭代中,您应该使代码效率低下也是有限度的。向代码添加完全不必要的切片,认为将来有人会花时间查找和优化它,这不是一种好风格,IMO。
-
它对 big-O 性能没有任何改进,它仍然是
O(n),所以它仍然会以相同的速率扩展......如果你在序列上操作的时间足够长,可以看到好处,我怀疑你的O(n)操作会成为瓶颈。 -
@TemporalWolf 嗯,你可能有一点。我想如果序列非常大,并且处理它们代表一个瓶颈,那么值得用
numpy/pandas/cython/ 重写它C 在很多情况下。我想,很多情况下加[::-1]也不是没有道理。 -
对于这么短的序列,额外的切片可能不会比其他处理边缘情况的方法更昂贵,并且复制甚至可能不是操作中最昂贵的部分。