1

New to python. How do I iterate completely through a list after starting in the middle (or at any point)? for example, if I have:

[a,b,c,d,e,f,g]

I want to start at d, then go e, f, g, a, b, c. But I am not sure how to do this. I want something to be capable of also going b, c, d, e, f, g, a, or any order that iterates completely through from any starting point.

I found this question discussing something similar, and an answer solved that problem by:

>>> a = [1, 2, 3, 4, 5, 6, 7]
>>> [a[(len(a) + (~i, i)[i%2]) // 2] for i in range(len(a))]
[4, 5, 3, 6, 2, 7, 1]

But, honestly, I don't know how to interpret how this code accomplishes this task. I do think that whatever I am looking for would be similar to this.

Kelly Bundy
  • 23,480
  • 7
  • 29
  • 65
YaGoi Root
  • 29
  • 6
  • 2
    Given a list `lst` and a "starting point" `p`: you want `lst[p:] + lst[:p]` – jedwards Mar 21 '22 at 22:43
  • 1
    @jedwards That makes unnecessary copies, compared with just indexing the list directly. – wim Mar 21 '22 at 22:48
  • In order to understand all the answers given you should know how the [modulo operator](https://en.wikipedia.org/wiki/Modulo_operation#:~:text=Given%20two%20positive%20numbers%20a,divisor%20one%20is%20operating%20from.) works, otherwise you won't get your head around these. In simple terms this allows an index that would be out of bounds of the array to instead *start at the front* again. It's probably best if you just use this `a = [1, 2, 3, 4, 5, 6, 7] start = 3 for i in range(len(a)): print((start + i) % len(a))` to see which indices are produced using the modulo operator. – Mushroomator Mar 21 '22 at 22:55

5 Answers5

3

Here's a very efficient iterator (since you used the iterator tag and said "iterate" multiple times). It uses only O(1) space and is very fast:

from itertools import islice, chain

def rotated(lst, start):
    it = iter(lst)
    next(islice(it, start, start), None)
    return chain(it, islice(lst, start))

lst = list('abcdefgh')
start = 3
print(*rotated(lst, start))

Output (Try it online!):

d e f g h a b c

Benchmark with a list of a million numbers, showing tracemalloc peaks and runtimes. Using the iterables suggested so far:

list_comprehension    8,697,776 bytes  1331.9 milliseconds
add_list_slices      16,000,000 bytes    28.4 milliseconds
iadd_list_slices     13,000,048 bytes    23.1 milliseconds
rotated_deque_1       8,243,328 bytes    26.9 milliseconds
rotated_deque_2             296 bytes     8.1 milliseconds
chained_list_slices   8,000,240 bytes    18.1 milliseconds
chained_iterators           360 bytes    11.6 milliseconds  <-- mine

The first deque solution is what the question asks for (starting with a list). The second is what ShadowRanger proposes (use a deque from the get-go, instead of a list).

Code (Try it online!):

import tracemalloc as tm
from timeit import default_timer as timer
from collections import deque
from itertools import islice, chain

def list_comprehension(a, start):
    return [a[(start + j) % len(a)] for j in range(len(a))]

def add_list_slices(a, start):
    return a[start:] + a[:start]

def iadd_list_slices(a, start):
    b = a[start:]
    b += a[:start]
    return b

def chained_list_slices(lst, start):
    return chain(lst[start:], lst[:start])

def rotated_deque_1(lst, start):
    d = deque(lst)
    d.rotate(-start)
    return d

def rotated_deque_2(lst, start):
    deck.rotate(-start)
    return deck
    # deck.rotate(start) is done in the benchmark code, after the iteration

def chained_iterators(lst, start):
    it = iter(lst)
    next(islice(it, start, start), None)
    return chain(it, islice(lst, start))

funcs = [
    list_comprehension,
    add_list_slices,
    iadd_list_slices,
    rotated_deque_1,
    rotated_deque_2,
    chained_list_slices,
    chained_iterators,
]

a = list(range(10))
start = 3
deck = deque(a)
for func in funcs:
    print(*func(a, start))

n = 10 ** 6
a = list(range(n))
start = n // 2
deck = deque(a)
for _ in range(3):
    print()
    for func in funcs:
        tm.start()
        t0 = timer()
        deque(func(a, start), 0)
        if func is rotated_deque_2:
            deck.rotate(start)
        t1 = timer()
        print(f'{func.__name__:{max(len(func.__name__) for func in funcs)}}  '
              f'{tm.get_traced_memory()[1]:10,} bytes  '
              f'{(t1 - t0) * 1e3:6.1f} milliseconds')
        tm.stop()
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
Kelly Bundy
  • 23,480
  • 7
  • 29
  • 65
1

You might try this approach:

>>> a = [1, 2, 3, 4, 5, 6, 7]
>>> start = 3
>>> [a[(start + j) % len(a)] for j in range(len(a))]
[4, 5, 6, 7, 1, 2, 3]

Another approach (this is unnecessary complex under the hood):

>>> a[start:] + a[:start]
[4, 5, 6, 7, 1, 2, 3]
Riccardo Bucco
  • 13,980
  • 4
  • 22
  • 50
  • 1
    The good thing about this approach is that it works even if start is a negative index. – wim Mar 21 '22 at 22:49
  • How is it "unnecessary complex" (esp. compared to the modulo operator, a list comprehension and `range(len(...))`)? That parenthetical makes no sense. – jedwards Mar 21 '22 at 23:22
  • @jedwards The space complexity is higher, because you're creating a useless copy of the list – Riccardo Bucco Mar 22 '22 at 13:44
  • @RiccardoBucco ..... as are you – jedwards Mar 22 '22 at 17:32
  • @jedwards `a[start:] + a[:start]` *triples* the space, though. Not sure about the list comp. [Experiments](https://tio.run/##bVJLbsIwFNznFG9TYUNABAqqInGFXiCKopfEoUaxHdlmgRBnT/1JG4rqVTKaN/Nm7OFmv5Tcfwx6HLkYlLZgNTZMYN@rBtCAFUmnlQDLBeMWJlLLOrz2tvKoDjT/kSQOh0aJgSDNE3BHM3vVEgosiLHoJldwofAGPZOOU0KnNFyAS9Aoz4xEmJZRCdv2VQiLIJOXTgiLPPxMbP5Mr@E0cydkdXoaedKs43yv1Ly3dPNxmV@54lNJVsISZID85jyFuuAujix9BiavrgW0jGAK62A0yfkzoDEvtuhduLEkps@2sFzCkdIkdnWCbLd/PxyTxJtVc027SdbDnYd956nvKw01pCHMbG3FJigSOkNbJx9u7QnsfvIGRvYPY9BcWtIt7t2mqiQKVlX54QFwdxZn5h6Efz1tJZhQ@kZokZV5tk0fUN8sM56Wwdp555t99wDDGiVbs6B/F3XXEJFoRsfxGw). – Kelly Bundy Mar 22 '22 at 21:56
1

It's not an answer given the code as written and the question as asked, but you might consider using a collections.deque instead of a list from the get-go. With deque, you can do a rotate operation prior to iterating, iterate normally (no complicated skipping/slicing logic), then (if you want/need to) rotate it back to the original position.

So for example, rather than building a list, you'd build a deque from the start:

from collections import deque

a = deque(range(1, 8))

a.rotate(-3)  # Shifts three elements from beginning to end
for x in a:   # deque is actually reordered, so just iterate normally
    print(x)
a.rotate(3)   # Shifts three elements from end back to beginning

which outputs:

4
5
6
7
1
2
3
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
0

In this solution i use range(len(items)) to generate a iterable object with values from 0 to the length of the list minus 1

That means that in this example in the for loop the value of the value of i will be 0,1,2,3... so i + startIndex will be 3, 4,5,...

This is almost what we want but we need to make sure we don't miss the elements at the beginning of the list. The % operator gets the remainder when (i + startIndex) is divided by the length of the list. This ensures the rest of the items are printed once i becomes greater than or equal to the length of the list as the remainder will be 0, 1, 2 ...

items = ["a", "b", "c", "d", "e", "f", "g"]
startIndex = 3

for i in range(len(items)):
    print(items[(i + startIndex) % len(items)])
Alex
  • 29
  • 4
0

As has been pointed out, you can just add together two slices:

>>> a[3:] + a[:3]
[4, 5, 6, 7, 1, 2, 3]

but the + operator creates a new list (even if you're only going to iterate through it once), which may not be desirable if the list is extremely long.

If you want the simplicity of the slice syntax and you want to be able to do a single iteration over the list elements without copying everything into a new list, there's itertools.chain, which lets you chain multiple iterables together without concatenating them into an in-memory list:

>>> import itertools
>>> for n in itertools.chain(a[3:], a[:3]):
...     print(n)
...
4
5
6
7
1
2
3
Samwise
  • 68,105
  • 3
  • 30
  • 44
  • *"without copying everything into a new list"* - Well... you're still copying everything... just into **two** new lists. – Kelly Bundy Mar 22 '22 at 21:59