【问题标题】:Reverse slice from end of list to a specific index从列表末尾反转切片到特定索引
【发布时间】: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]也不是没有道理。
  • 对于这么短的序列,额外的切片可能不会比其他处理边缘情况的方法更昂贵,并且复制甚至可能不是操作中最昂贵的部分。

标签: python slice


【解决方案1】:

如果迭代器结果没问题,使用前向切片并在其上调用reversed

reversed(seq[seq.index(whatever):])

如果不是,则从端点中减去一个额外的len(seq)

seq[:seq.index(whatever)-len(seq)-1:-1]

或者只是取一个正向切片,再次切片以反转它,并吃掉额外副本的成本。这可能不是你的瓶颈。

无论你做什么,留下评论解释它,这样人们在编辑时就不会重新引入错误,并为此案例编写单元测试。

【讨论】:

  • 哦,对了,从端点中减去 len(seq) 是可行的。不过,它肯定需要评论,但一点也不明显!我希望 python slice 能更好地处理这个问题,但我已经晚了 20 年才提出修复:)
  • 您从使用第二个中看到的性能提升几乎可以保证不会抵消额外的维护负担。
  • @TemporalWolf 我同意这样做的维护负担非常高。
  • @max 另外,根据repl.it,它更慢。所以这是双输。
  • @max:切片不承诺返回一个新对象。内置序列类型承诺改变切片不会影响原始对象,但对于不可变序列,此承诺实际上并不需要创建新对象。 Here's a demo.
【解决方案2】:

恕我直言,seq[seq.index(x):][::-1] 是最易读的解决方案,但这里有一种更高效的方法。

def sliceback(seq, key):
    pos = seq.index(key)
    return seq[:pos-1 if pos else None:-1]

seq = 'abc'
for k in seq:
    print(k, sliceback(seq, k)) 

输出

a cba
b cb
c c

正如 Budo Zindovic 在 cmets 中提到的,如果在字符串中找不到字符,.index 将引发异常。根据上下文,代码可能永远不会使用不在seq 中的字符调用,但如果可能,我们需要处理它。最简单的方法是捕获异常:

def sliceback(seq, key):
    try:
        pos = seq.index(key)
    except ValueError:
        return ''
    return seq[:pos-1 if pos else None:-1]

seq = 'abc'
for k in 'abcd':
    print(k, sliceback(seq, k)) 

输出

a cba
b cb
c c
d 

Python 异常处理非常高效。当异常实际上没有被引发时,它比等效的基于if 的代码要快,但如果异常被引发超过5-10% 的时间,则使用if 更快。

比起在调用seq.index 之前测试key,使用find 更有效。当然,这只有在seq 是一个字符串时才有效;如果 seq 是一个列表,它将不起作用,因为(令人讨厌)列表没有 .find 方法。

def sliceback(seq, key):
    pos = seq.find(key)
    return '' if pos < 0 else seq[:pos-1 if pos else None:-1]

【讨论】:

  • 这具有使pos == 0 处理更加明确的优势,希望人们更有可能意识到它为什么存在。 (另一方面,人们必须明白None 相当于一个省略的端点。)
  • 我不同意编辑。我说我们应该让异常传播,而不是捕获ValueError
  • 在性能方面,我看到使用 sliceback 比简单解决方案提高了约 10%:repl.it
  • @user2357112 好的。允许异常冒泡也很有意义。我想这取决于 OP 对结果的处理方式。
【解决方案3】:

您可以在分配字符串时检查pos,例如:

result = seq[-1:pos-1:-1] if pos > 0 else seq[::-1]

输入:

pos = seq.index('a')

输出:

cba

【讨论】:

  • 当然可以。我只是希望有一些不那么冗长的东西。
  • 当找不到字符时,请注意pos 值的大小写。
  • 好点@BudoZindovic!我假设由于 OP 使用的是 ,index 而不是发现他们的代码正确地包装在 try... except ValueError: 中,或者有其他方法来确保他们不提供不在字符串中的字符。 ;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-04-26
  • 2010-10-11
  • 1970-01-01
  • 2014-05-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多