39

A specific example of my question is, "How can I get '3210' in this example?"


>>> foo = '0123456'
>>> foo[0:4]
'0123'
>>> foo[::-1]
'6543210'
>>> foo[4:0:-1] # I was shooting for '3210' but made a fencepost error, that's fine, but...
'4321'
>>> foo[3:-1:-1] # How can I get '3210'?
''
>>> foo[3:0:-1]
'321'

It seems strange that I can write foo[4:0:-1], foo[5:1:-1], etc. and get what I would expect, but there's no way to write the slice so that I get '3210'.

A makeshift way of doing this would be foo[0:4][::-1], but this creates two string objects in the process. I will be performing this operation literally billions of times, so every string operation is expensive.

I must be missing something silly and easy. Thanks for your help!

eblume
  • 1,740
  • 3
  • 17
  • 24
  • "but this creates two string objects in the process" not necessarily - internally Python is free to do whatever optimizations it can get away with without breaking code, and since strings are semantically immutable, it could do slice objects that present themselves as strings that point to the same memory as the original string. It could also be coded to notice that there are no other references to the first slice object, and reuse the first slice object as second by just modifying the values in it in-place. Does it actually? Probably not. But PyPy's JIT might, and future Python could. – mtraceur May 28 '22 at 10:21

8 Answers8

49

Simply exclude the end range index...

>>> foo[3::-1]
'3210'

Ironically, about the only option I think you didn't try.

Andrew White
  • 52,720
  • 19
  • 113
  • 137
  • 5
    The problem there is that this slice operation is being used in an algorithm that works somewhat like as follows: foo[i:i-4:-1], and starts with a high 'i' and walks down. I can't just remove the 'i'. I suppose I could use an edge case and say "if i-4 < 0, use this other slice". – eblume Apr 27 '11 at 16:46
  • 1
    I'll set this as 'accept' if it's the only way, but using the edge-case of removing the middle operand for the final 'reverse substring' (and not for any of the other reverse substrings) seems clunky. – eblume Apr 27 '11 at 16:49
  • 2
    well, to be fair you didn't include that condition in your question and even then you can wrap that "clunky" logic in a function and be done with it. – Andrew White Apr 27 '11 at 17:09
  • Could you explain why exactly foo[len(foo)-1:-1:-1] does not work here? What's the harm in the -1 stop index, and why does it require None? – Alice Feb 06 '15 at 17:54
  • 1
    @Alice -1 is always the last element of the list (or the space between it and the preceding element, depending on your view of list indices). I agree, when using a negative step it would be nice if -1 meant the space beyond the start of the list, like how len(L) means the space beyond the end of the list. If you use -1 as the stop and step -1, you'll have an empty slice. – BallpointBen Nov 30 '16 at 19:46
  • 2
    If you wanted to use negative indices, you would have to count the number of negative steps, something like `s[len(s)-1 : - (len(s)+1) : -1]` – Anindya Dutta Dec 08 '17 at 13:02
11

Omit the end index in your slice notation:

>>> foo = '0123456'
>>> foo[3::-1]
'3210'

If you have to do this many times, create a slice object that you can use over and over

>>> first_4_items_reversed = slice(3,None,-1)
>>> foo[first_4_items_reversed]
'3210'
PaulMcG
  • 62,419
  • 16
  • 94
  • 130
8

If you're looking for something a little more human-readable than extended slice notation:

>>> foo = '0123456'
>>> ''.join(reversed(foo[0:4]))
'3210'
Aaron Dufour
  • 17,288
  • 1
  • 47
  • 69
  • 4
    Thanks, but no, this creates *three* memory objects. join and reversed are pretty fast, but I think it's safe to assume that extended slice notation will be faster. – eblume Apr 27 '11 at 16:48
  • 4
    I generally run under the assumption that if you're writing in python, efficiency isn't the most important thing. The only other thing that could be important is readability, so I optimize for that. Extended slice notation, particularly with negative step values, comes across as very opaque. – Aaron Dufour Apr 27 '11 at 21:03
  • @eblume *ideas* don't have speed and don't create memory objects - implementations do. If you're worried about speed in Python, start by using PyPy, not micro-optimizing how you reverse things. And in the meantime, remember that any slice notation optimization you can conceive of in principle can be done in `reversed` - and since in Python it's much easier to monkeypatch/shadow functions from the outside than operators, you could literally write your code to use `reversed` and then just in the namespace of hot loops do `reversed = my_epic_optimized_c_reversed`. – mtraceur May 28 '22 at 09:51
  • @eblume in any case it's not safe to assume that, because a sufficiently advanced hypothetical optimizer would understand a construct like `''.join(reversed(foo[x:y]))` and would just generate code which just directly copies characters backwards from `foo` from index y-1 to index x directly into a new string's backing memory allocated with knowledge that `y-x` was the needed size. And in the meantime, JIT-optimizing VMs like PyPy (and modern compilers) are getting impressively close to those kinds of optimizations - and the clearer the code, the more it naturally lends itself to optimizers. – mtraceur May 28 '22 at 09:59
7

After reading the "technical documentation" (here) - specifically the sentence:

If either bound is negative, the sequence’s length is added to it.

I decided to try this, and it worked:

>>> foo = '0123456'
>>> foo[3:-1-len(foo):-1]
'3210'
>>>

So I think the best answer to programmatically determine the "end point" would be to provide a well named helper function that makes it clear that its arguments are always treated like positive offsets, maybe special_slice()

I think the clarity of this 'special' case is extremely important since lots of common and significant use cases depend on the default behavior of negative offsets (i.e. adding the length to them). Personally I frequently use a '-1' end point to mean: stop just before last element.

So, based on your comment:

... algorithm that works somewhat like as follows: foo[i:i-4:-1], and starts with a high 'i' and walks down.

I might make the following:

def slice_by_len(data, start, length, step=1):
    end = start + length if step > 0 else start - length
    if end < 0:
        # Fix the negative offset to get what we really want
        end -= len(data)
    return data[start:end:step]

And then call it for each slice required:

foo_part = slice_by_len(foo, i, 4, -1)

The above could easily go in a loop over values of 'i'

CrashNeb
  • 394
  • 3
  • 12
  • 2
    It's ugly trying to achieve this in Python! I've referenced your answer [here](https://stackoverflow.com/a/49811961/5353461). – Tom Hale Apr 13 '18 at 13:42
4

You can use s[::-1] to reverse the entire string. But if you want to reverse each substring with some fixed length, you can first extract the substring and then reverse the entire substring. For example, let's assume we need to check whether each substring with length 3 of string foo is a palindrome, we can do it like this:

>>> foo = '0102030'
>>> for i in range(len(foo)-3):
...     if foo[i:i+3] == foo[i:i+3][::-1]:
...         print(foo[i:i+3], 'is a palindrome')
...     else:
...         print(foo[i:i+3], 'is not a palindrome')
...
010 is a palindrome
102 is not a palindrome
020 is a palindrome
203 is not a palindrome
030 is a palindrome

If you want to check if a substring is palindrome like this:

if foo[i:i+3] == foo[i+2:i-1:-1]:
    ...

you will not be able to handle the case of i being 0, since you are actually comparing foo[0:3] with foo[2:-1:-1], which is equivalent to foo[2:n-1:-1], which in turn is an empty string.

The only drawback of the first solution is that it uses a little more memory but it's no big deal.

Luan Gong
  • 41
  • 4
3

Given:

>>> foo = '0123456'

The desired string 3210 is from index 3rd to the 0-th characters:

>>> stop_idx=0
>>> start_idx=3

Here are two generic solutions:

  1. Take the forward slice then reverse it:

    >>> foo[stop_idx:start_idx+1][::-1]
    '3210'
    
  2. Based on this answer, use a negative step and stop 1 element before the first element (plus the stop offset):

    >>> foo[start_idx:stop_idx-len(foo)-1:-1]
    '3210'
    
    >>> a[start_idx:stop_idx-len(a)-1:-1]
    [2, 1]
    

Comparing execution times, the first version is faster:

>>> timeit.timeit('foo[stop_idx:start_idx+1][::-1]', setup='foo="012345"; stop_idx=0; start_idx=3', number=10_000_000)
1.7157553750148509
>>> timeit.timeit('foo[start_idx:stop_idx-len(foo)-1:-1]', setup='foo="012345"; stop_idx=0; start_idx=3', number=10_000_000)
1.9317215870250948
Tom Hale
  • 40,825
  • 36
  • 187
  • 242
1

In addition to the above solutions, you can do something like:

foo = '0123456'
foo[-4::-1]

I guess if foo is going to be changing lengths, this may not be the best solution, but if the length is static it would work.

jack
  • 111
  • 5
-2
s="this is my world"
pattern=re.findall(r'\S+',s)
a=[]
for i in range(len(pattern)):
    a.append((pattern[i][::-1]))
print (a)
print (" ".join(a))