从列表末尾到特定索引

时间:2017-05-30 19:53:08

标签: python slice

假设我需要从序列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]通常都很好。

3 个答案:

答案 0 :(得分:3)

如果迭代器结果没问题,请使用转发片并在其上调用reversed

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

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

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

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

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

答案 1 :(得分:3)

恕我直言,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在评论中提到的,如果在字符串中找不到字符,.index将引发异常。根据上下文,可能不会使用不在seq中的char调用代码,但如果可能,我们需要处理它。最简单的方法是捕获异常:

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会更快。

在调用key之前,不是测试seq.index,而是使用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]

答案 2 :(得分:0)

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

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

输入:

pos = seq.index('a')

输出:

cba