Slice a list based on an index and items behind it in Python
Asked Answered
V

16

39

Say I have an array of degree values, like this:

DEGREES = [
    0, 15, 30, 45, 60,
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]

I would pick an angle and then be able to bisect this hypothetical circle in order to make it easier to find the shortest route to the target direction.

Saying that, how can I pick a specific value, like 90, and then be able to find the previous 12 elements behind that, including the index wrapping around to the end?

So, taking that earlier value and applying to that list, I would get something like this:

[90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]

Using slice notation, I tried doing this:

index = DEGREES.index(90)
print(DEGREES[index-12:index]) # start 12 values back, stop at index

But this only prints an empty array.

Is there a way to slice a list so I can get the 12 previous values behind the index I'm using?

EDIT:

This turned out to be an XY Problem, my bad. Originally, I was trying to create a smooth rotation system in Pygame, with my attempts to calculate angles not working, I asked this question to solve a problem with yet another idea I was trying to implement. I ended up accepting the answer that helped me set up the smooth rotation system, but there are relevant answers to the original question below that.

Vituline answered 14/5, 2019 at 17:42 Comment(7)
This might help: https://mcmap.net/q/409179/-pythonic-circular-listInfatuated
The dupe is wrong. In the question of the dupe it specifically states that he wants to grab the index of an arbitrary element and the values of its neighbors. Which is a clearly different questionBallman
Is DEGREES just an example or will you only use this slicing with increasing values and a constant increment?Hager
What Eric Duminil is likely getting at is that there's no need to construct such a list if you're just incrementing by a constant value. This list could just as easily be represented by something like range(0,360,15) and you could calculate the information you're after on the fly: [i%360 for i in range(90,90-15*13,-15)]Recor
@zephyr: ExactlyHager
@EricDuminil This array of degrees is designed to work with a smooth rotation system that I'm trying to create in pygame. Normally I would just find the difference between the current direction and the target direction and increment from there, but since the rotation rolls over at zero I have to hardcode the values to make sure that it will always go the shortest route possible. Its a bit more complex than that but to keep a concise question I left why I'm doing this out.Vituline
@Pygasm: You really should mention what your actual goal is in the question (not just in comments) because otherwise you might get many answers which miss the point. I dare say that my answer will help you.Hager
H
40

Arithmetic with angles

Your goal isn't to slice, concatenate or reverse lists. Your goal is to do basic arithmetic with degrees and keep the results between 0 and 359. For this, you really should use the modulo operator %:

>>> 90 % 360
90
>>> 390 % 360
30
>>> -60 % 360
300
>>> 360 % 360
0

Back to the question

If you only want to use this slicing for degrees with a constant increment, you could generate the desired list directly:

>>> STEP = 15
>>> list(range(0, 360, STEP))
[0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, 285, 300, 315, 330, 345]
>>> def previous_degrees(start, n, step=STEP):
...     return [(start - i * step) % 360 for i in range(n + 1)]
... 
>>> previous_degrees(90, 12)
[90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]
>>> previous_degrees(90, 12, 30)
[90, 60, 30, 0, 330, 300, 270, 240, 210, 180, 150, 120, 90]
>>> previous_degrees(90, 6, 45)
[90, 45, 0, 315, 270, 225, 180]

Your real question

You wrote in a comment:

This array of degrees is designed to work with a smooth rotation system that I'm trying to create in pygame. Normally I would just find the difference between the current direction and the target direction and increment from there, but since the rotation rolls over at zero I have to hardcode the values to make sure that it will always go the shortest route possible.

From two angles, you need to determine if you should turn clockwise or anticlockwise. You can use modulo again to make sure that the rotation will be between -180° and 179°:

def shortest_rotation(start_angle, end_angle):
    return (end_angle - start_angle + 180) % 360 - 180

Here's an example:

>>> shortest_rotation(0, 90)
90
>>> shortest_rotation(90, 0)
-90
>>> shortest_rotation(90, 90)
0
>>> shortest_rotation(90, 330)
-120
>>> shortest_rotation(0, 180)
-180
>>> shortest_rotation(0, 181)
-179
>>> shortest_rotation(0, 179)
179
>>> shortest_rotation(10, 350)
-20

You can now create a list of angles, turning in the shortest direction:

def rotation_steps(start_angle, end_angle, n):
    increment = shortest_rotation(start_angle, end_angle) / n
    return [(start_angle + i * increment) % 360 for i in range(n + 1)]

As an example:

>>> rotation_steps(90, 270, 12)
[90.0, 75.0, 60.0, 45.0, 30.0, 15.0, 0.0, 345.0, 330.0, 315.0, 300.0, 285.0, 270.0]
>>> rotation_steps(10, 350, 2)
[10.0, 0.0, 350.0]

The list uses float in order to avoid missing the end_angle if increment isn't an integer.

Hager answered 14/5, 2019 at 18:43 Comment(4)
This feels a lot more elegant than the other solutions I'm seeing, I'll try it today and then come back if it works or not. I do apologize that I didn't exactly include why I wanted this system in the first place, I felt like I was going to be downvoted/ignored if I tried to explain further than the basic premise of the problem.Vituline
@Pygasm Don't worry, it's hard to write a good question, and it's hard to include enough information yet not write too long a question. See XY Problem.Hager
Worked like a charm. Thank you so much, I'll try to not make an XY problem next time.Vituline
@Pygasm My pleasure! Please update your question with the comment in which you explain your real goal. Comments can be deleted more easily than questions.Hager
H
18

Or you could use a deque:

from collections import deque
from itertools import islice

dq = deque(reversed((0, 15, 30, 45, 60,
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345)))

index = dq.index(90)
dq.rotate(-index)
res = list(islice(dq, 13))
# [90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]

You could use that as a function:

def f(i):
    dq.rotate(-dq.index(i))
    return list(islice(dq, 13))

#  f(90) = [90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]
Helman answered 14/5, 2019 at 17:51 Comment(0)
T
11

Something like this might be more direct:

index = DEGREES.index(90)
print([DEGREES[i] for i in range(index, index-13, -1)])
Troat answered 14/5, 2019 at 17:53 Comment(0)
B
7

For these cases, a NumPy function that comes in handy is np.roll, which, as its name specifies, rolls the elements in the array, and as also as mentioned in the documentation:

Elements that roll beyond the last position are re-introduced at the first

Which is exactly what we need in order to roll at the back the first items in the list up to the index where 90 appears.

So one approach could be to use the index where 90 appears using the index list method, and shift the array up to -k positions, k being the given index. Then we can just slice the list and take its last n elements reversed:

import numpy as np

l = [
    0, 15, 30, 45, 60,
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]

def roll_n_reversed(l, i, n):
    return np.roll(l, -l.index(i)-1)[:-(n+1):-1]

roll_n_reversed(l, 90, 13)

Which yields the expected output:

array([ 90,  75,  60,  45,  30,  15,   0, 345, 330, 315, 300, 285, 270])
Ballman answered 14/5, 2019 at 17:49 Comment(7)
if it's not a sorted array , don't you think in that case it will fail ?Peper
Updated the solution @prashantrana now it works without assuming the array is sortedBallman
Hmm @EricDuminil I did have the l.index(90) inside the key arg which was a mistake, should scale linearly nowBallman
It looks better now. Sorting probably isn't needed but there's not much difference between O(n.log n) and O(n) anyway.Hager
Yes well the whole idea is to simply rearange based on whether an item is bellow l.index(90) or not. Then its just a matter of slicing. Thanks for pointing out @EricDuminilBallman
i don;t know, but why dont just create a new list where reverse of list (all element upto 90 ) and reverse of list (from 90 + next value to end ) and from this final list just get the first 12 element, no need to sort (op didn't mentioned sort need if need then fter sort use like this ) with sort (O(N)) else O(n)Peper
@Ballman not doubting your answer, what I want to say for the given OP problem, instead of sorting and then doing the extra operation and then giving result, by doing sicing is better , and let us save time and computationPeper
H
7

An itertools (cycle and islice) based solution:

from itertools import cycle, islice

DEGREES = cycle(reversed((
    0, 15, 30, 45, 60,
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345)))

next(item for item in DEGREES if item == 90)  # advance to next 90
res = [90] + list(islice(DEGREES, 12))
# [90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]

You can pack that into a one-liner function:

def f(i):
    return [next(d for d in DEGREES if d == i), *islice(DEGREES, 12)]

#  f(90) = [90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]

Or even using dropwhile (as mentioned in the comments):

from itertools import cycle, islice, dropwhile

def f(i):
    return list(islice(dropwhile(lambda d: d != i, DEGREES), 13))

If your list is exactly as you printed above, you could also generate the slices on the fly using range:

def f(i, d=15, n=13):
    return [deg % 360 for deg in range(i, i-n*d, -d)]

# f(90) = [90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]
Helman answered 14/5, 2019 at 18:4 Comment(4)
I also think OP should simply use modulo and not bother with itertools, slicing, reversing or concatening. Your one-liner is now very similar to mine, but it might be needed in order to get attention because the signal to noise is pretty low for this question. ;)Hager
ooops. sorry, did not mean to infringe on your answer. i could remove that part or add a link to your answer. what would you prefer? (+1 anyway!)Helman
no problem, you can leave it like this. We need to fight against 13 other answers. ;)Hager
@EricDuminil the democratic process here seems to have taken care of it. that is reassuring!Helman
G
4

You can use this:

previous12 = [DEGREES[p-i] for p in [DEGREES.index(90)] for i in range(13)]

or this:

previous12 = (DEGREES+DEGREES[:DEGREES.index(90)+1])[:-14:-1]
Gristmill answered 14/5, 2019 at 19:29 Comment(0)
T
3

You cannot do that with one slice unfortunately. You can either concatenate the pieces, which can be a bit awkward:

DEGREES = [
    0, 15, 30, 45, 60, 
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]

index = DEGREES.index(90)
result = DEGREES[index:index - 12:-1] if index >= 12 else (DEGREES[index::-1] + DEGREES[:index - 12:-1])
print(result)
# [90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285]

Or just use a list comprehension:

DEGREES = [
    0, 15, 30, 45, 60, 
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]

index = DEGREES.index(90)
result = [DEGREES[i] for i in range(index, index - 12, -1)]
print(result)
# [90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285]
Thirteen answered 14/5, 2019 at 17:53 Comment(0)
T
1

I think itertools.chain might be useful here:

from itertools import chain

DEGREES = [
    0, 15, 30, 45, 60,
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345
]

def get_list_of_degrees(degree, resulting_list_length):
    index = DEGREES.index(degree)
    lower_index = index - (resulting_list_length)
    if index >= resulting_list_length:
        result = DEGREES[lower_index: index]  # start 12 values back, stop at index
    else:
        result = list(chain(DEGREES[lower_index:], DEGREES[:index])) # start 12 values back, stop at index
    return result

my_degrees = get_list_of_degrees(90, 12)
print(my_degrees)

Yields:

[270, 285, 300, 315, 330, 345, 0, 15, 30, 45, 60, 75]

Which is what you specified, just backwards

Perhaps a more straightforward and scaleable/alterable method would be to generate the angles on the fly without a DEGREES list. Something like:

def get_angles(start_angle=90, increment=-15, return_array_size=12):
    angles = [i for i in range(start_angle + increment, start_angle + (return_array_size*increment) + increment, increment)]
    for index in range(len(angles)):
        while angles[index] < 0:
            angles[index] += 360
    return angles

print(get_angles())

Returns:

[75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]

While allowing you the flexibility to return only 5 angles easily, or go in step sizes of 2 degrees, etc.. For instance

print(get_angles(increment=-2))

Now returns:

[88, 86, 84, 82, 80, 78, 76, 74, 72, 70, 68, 66]

With very minimal change done to your code (otherwise you would have to generate a new DEGREES array to accomplish this)

Tillio answered 14/5, 2019 at 17:50 Comment(3)
The issue there is that I need the values at the end as well, so the list would pass zero and keep adding values (345, 330, etc) like it was slicing with a negative integer.Vituline
@Pygasm I misunderstood! Is this more what you were looking for?Tillio
start_angle should be the first element in the list. You're correct that it's possible to generate the list directly, but it's much easier with modulo. [i for i in something] is just list(something) and can often be replaced by something.Hager
N
1

The reason you got empty list is because you simply don't have 12 items prior to value of 90.

What you need is a statement to handle this exception:

index = DEGREES.index(90)
if index >= 12:
    print(DEGREES[index-12:index])
else: 
    print(DEGREES[:index])
Northeastwards answered 14/5, 2019 at 17:51 Comment(1)
I initially put this answer, and OP said "The issue there is that I need the values at the end as well, so the list would pass zero and keep adding values (345, 330, etc) like it was slicing with a negative integer." I don't think this is suitable for OP. They specifically want to wrap around the list and get all 12 elements, instead of shortening their # of output elementsTillio
P
1

By list slicing:

DEGREES = [
    0, 15, 30, 45, 60,
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]

value = 90
index = DEGREES.index(value)



result = DEGREES[:index+1][::-1] + DEGREES[index+1:][::-1]
result = result[:13]
print(result)

Output

[90, 75, 60, 45, 30, 15, 0, 345, 330,
 315, 300, 285, 270]

or

RES= [ DEGREES[i] for i in range(index,index-12-1,-1)]
Peper answered 14/5, 2019 at 17:52 Comment(0)
S
1

I think you need to do some arithmetic.

index = DEGREES.index(90) + 1
offset = 12
start = index - offset
length = len(DEGREES)
print(
    list(reversed(DEGREES[max(0, start):index])) + 
    (list(reversed(DEGREES[length + start - 1 :length])))
     if start < 0
     else [])
)

Alternatively:

Stanford answered 14/5, 2019 at 17:53 Comment(0)
L
1

I would suggest you to try itertools.cycle() for any number of previous values.

Just reverse the list and try cycle().

import itertools

degrees = [0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, 285, 300, 315, 330, 345]
n=12
degrees.reverse()
ind = degrees.index(90)
degrees = degrees[ind:]+degrees[:ind]
rev_cycle = itertools.cycle(degrees)
for i in range(n+1):
    print(next(rev_cycle))

This is efficient as it is using generators.

Lifework answered 14/5, 2019 at 17:58 Comment(0)
S
1

In your example, the elements you wish to print out are DEGREES[-6:6]. You might want to add conditionals to take care of starting indexes that end up looping back around. Something like this:

DEGREES = [
    0, 15, 30, 45, 60, 
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]

index = DEGREES.index(90)
start_idx = index - 12
if start_idx < 0:
    print(DEGREES[start_idx:] + DEGREES[:index + 1])
else:
    print(DEGREES[start_idx:index + 1])

this should return the following:

[270, 285, 300, 315, 330, 345, 0, 15, 30, 45, 60, 75, 90]

which is your solution, but reversed.

Straightout answered 14/5, 2019 at 17:58 Comment(1)
The expected output differs from the actual for this solution. :-)Lifework
A
1
DEGREES = [
    0, 15, 30, 45, 60, 
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]


index = DEGREES.index(90)

subFront = DEGREES[:index + 1][-12:]
subFront.reverse()

remainLen = 12 - len(subFront) + 1
if remainLen > 0:
    subBack = DEGREES[-remainLen:]
    subBack.reverse()
    subFront = subFront + subBack
print(subFront)
[90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]
Adhibit answered 14/5, 2019 at 18:2 Comment(0)
M
1

Or

import numpy as np

DEGREES = [
    0, 15, 30, 45, 60,
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]
idx = DEGREES.index(90)
new_list = DEGREES[::-1]
newList = np.roll(new_list, idx+1)
print(newList)
Maddy answered 14/5, 2019 at 18:12 Comment(0)
C
1

I have this handy function that implements wrapping slicing. While your use case might be better solved by directly computing the angle values as other answers have already shown. This might do the trick:

def wrapping_slice(lst, *args):
    return [lst[i%len(lst)] for i in range(*args)]

Example:

DEGREES = [
    0, 15, 30, 45, 60, 
    75, 90, 105, 120,
    135, 150, 165, 180,
    195, 210, 225, 240,
    255, 270, 285, 300,
    315, 330, 345,
]

start = DEGREES.index(90)

print(wrapping_slice(DEGREES, start, start-13, -1))

The output is:

$ python test.py
[90, 75, 60, 45, 30, 15, 0, 345, 330, 315, 300, 285, 270]
Clad answered 15/5, 2019 at 8:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.