1

Before anything: I did read Wrapping around a python list as a slice operation and wrapping around slices in Python / numpy

This question is not a duplicate of any of those two questions simply because this question is a totally different question. So stop downvoting it and do not mark it as a duplicate. In the first mentioned thread, the "wrap" there means something different. For the second mentioned thread, they dealt with ndarray and can only work for integers only.

Real question: How to slice a string or an array from a point to another point with an end between them?

Essentially, we want to do something like this,

n = whatever we want
print(string[n-5:n+6])

The above code may look normal. But it doesn't work near the edges (near the beginning of the string/array or the end of the string/array). Because Python's slicing doesn't allow slicing through the end of the array and continuing from the beginning. What if n is smaller than 5 or length of string longer than n+6?

Here's a better example, consider that we have

array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']

We want to print an element with its nearest two neighbors in string for all elements in an array

print("Two neighbors:")
for i, x in enumerate(array):
    print(array[i-1] + array[i] + array[(i+1)%len(array)])

Output:

Two neighbors:
kab
abc
bcd
cde
def
efg
fgh
ghi
hij
ijk
jka

So far so good, let's do it with four neighbors.

print("Four neighbors:")
for i, x in enumerate(array):
    print(array[i-2] + array[i-1] + array[i] + array[(i+1)%len(array)] + array[(i+2)%len(array)])

Output:

Four neighbors:
jkabc
kabcd
abcde
bcdef
cdefg
defgh
efghi
fghij
ghijk
hijka
ijkab

You can see where this is going, as the desired number of neighbors grow, the number of times we must type them out one by one increases.

Is there a way instead of s[n-3]+s[n-2]+s[n-1]+s[n]+s[n+1]+s[n+2]+s[n+3], we can do something like s[n-3:n+4]?

Note that s[n-3:n]+s[n:(n+4)%len(s)] doesn't work at the edges.

NOTE:

For the particular example above, it is possible to do a 3*array or add a number of elements to the front and to the back to essentially "pad" it.

However, this type of answer cost a bit of memory AND cannot work when we want to wrap it many folds around.

Consider the following,

# len(string) = 10
# n = 0 or any number we want
print(string[n-499:n+999])

If the start and end indices can be flexible instead of mirroring each other(eg. string[n-2:n+9] instead of string[n-3:n+4]), it is even better.

Sayyora
  • 463
  • 5
  • 17
  • 1
    I'd do something like this: (3*array)[len(array)+n-i:len(array)+n+i+1] where n is the middle index and i is the number of desired neighbours at each side. – kszl Feb 05 '18 at 15:34
  • 1
    @BUZZY I did almost the same workaround by just adding i numbers to the beginning and the end, and then adding i to the desired indices. This saves more memory. But both your and my workaround is not Pythonic enough. – Sayyora Feb 05 '18 at 15:37

4 Answers4

1

This could give ideas. The only thing to check is the order in your interval. Works with any n.

array = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']

def print_neighbors(n_neighbors):
    for idx in range(len(array)):
        start = (idx- n_neighbors//2) % len(array)
        end = (idx+n_neighbors//2) % len(array) + 1
        if start > end:
            print(''.join(array[start:] + array[:end]))
        else:
            print(''.join(array[start:end]))

>>> print_neighbors(6)

ijkabcd
jkabcde
kabcdef
abcdefg
bcdefgh
cdefghi
defghij
efghijk
fghijka
ghijkab
hijkabc
arnaud
  • 3,293
  • 1
  • 10
  • 27
  • Good classic answer, the start and end are both flexible and easy to understand. Although I initially wanted to avoid the conditional statements but just comparing the indices is surprisingly comfortable to read. Always wondered why native slicing doesn't do this. – Sayyora Feb 05 '18 at 16:04
  • @Sayyora This solution is inefficient because it relies on an `if`/`else`. The `if`/`else` here will lead to branch misprediction in the CPU and you can solve this entirely with a loop. – Nick Chapman Feb 05 '18 at 16:04
  • @NickChapman Yes I wanted to avoid conditional statements if possible too. See my other comment: By just adding i elements to the front and the back, we can essentially "wrap" the array around. However, that takes a bit of memory. And moreover, it doesn't work if we want to "wrap" it a hundred fold. Will add to question. – Sayyora Feb 05 '18 at 16:08
  • @Sayyora if you're looking for memory efficiency I believe my answer has that. – Nick Chapman Feb 05 '18 at 16:10
1

A solution which doesn't use an excessive amount of memory is as follows

my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

def get_sequences(a_list, sequence_length):
    sequences = []
    for i in range(len(my_list)):
        sequences.append("".join(str(my_list[(x + i) % len(my_list)]) for x in range(sequence_length)))
    return sequences

print(get_sequences(my_list, 2))
print(get_sequences(my_list, 3))

will output

['12', '23', '34', '45', '56', '67', '78', '89', '91']
['123', '234', '345', '456', '567', '678', '789', '891', '912']

This is nice because it utilizes a generator everywhere that it can.

Nick Chapman
  • 4,402
  • 1
  • 27
  • 41
  • Perfect answer. Can modify to make start and end flexible, is easy to read, very Pythonic style, memory efficient and essentially is able to have infinite folds. Just an extra, if I want it to have 2 neighbors in front and 5 neighbors in the back (flexible slicing), how will you write it? – Sayyora Feb 05 '18 at 16:19
0

You could create a class to wrap your original iterable like this:

class WrappingIterable():
     def __init__(self, orig):
         self.orig=orig
     def __getitem__(self, index):
         return self.orig[index%len(self.orig)]
     def __len__(self):
         return len(self.orig)


>>> w = WrappingIterable("qwerty")
>>> for i in range(-2, 8):
...     print(w[i])
t
y
q
w
e
r
t
y
q
w
Turn
  • 6,656
  • 32
  • 41
0

For this particular issue you can use a snippet like this:

def print_neighbors(l, n):
   wrapped = l[-(n//2):] + l + l[:(n//2)]
   for i in range(len(l)):
     print(''.join(wrapped[i:i+n+1]))

l = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']
print_neighbors(l, 2)
print_neighbors(l, 4)

Hope it makes sense!

Szabolcs
  • 3,990
  • 18
  • 38