Python reverse-stride slicing
Question:
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!
Answers:
Simply exclude the end range index…
>>> foo[3::-1]
'3210'
Ironically, about the only option I think you didn’t try.
If you’re looking for something a little more human-readable than extended slice notation:
>>> foo = '0123456'
>>> ''.join(reversed(foo[0:4]))
'3210'
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'
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.
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.
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’
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))
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:
-
Take the forward slice then reverse it:
>>> foo[stop_idx:start_idx+1][::-1]
'3210'
-
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
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!
Simply exclude the end range index…
>>> foo[3::-1]
'3210'
Ironically, about the only option I think you didn’t try.
If you’re looking for something a little more human-readable than extended slice notation:
>>> foo = '0123456'
>>> ''.join(reversed(foo[0:4]))
'3210'
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'
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.
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.
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’
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))
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:
-
Take the forward slice then reverse it:
>>> foo[stop_idx:start_idx+1][::-1] '3210'
-
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