0

The essence:

With a list like ['A', 'B', 'C'], I'm trying to build or find a function that will return the next element of the list each time it's called. And if it, in this case, is called more than three times, then A will be returned. And after that B etc.


The details and what I've tried:

I've got a list of colors ['A', 'B', 'C'] that I'm applying to different objects. The number of objects may vary each time the code is run. If I've got more objects than colors in the list, I'd like the fourth object to have the color 'A'. So, as the title says, I'd like to loop through a list by and index and start from the beginning if index > len(list). The following custom function looper will do just that.

def looper(lst, runs):
    j = 0
    for i in range(0, (len(lst)+(runs-(len(lst))))):
        if i < len(lst):
            print(lst[i])
        else:
            print(lst[j])
            j +=1

Test 1

# input:
runs = 2
looper(['A', 'B', 'C'], runs)

# output:
A
B

Test 2

#input :
runs = 5
looper(['A', 'B', 'C'], runs)

#output :
A
B
C
A
B

As you can see, this simple function will break if runs gets large enough. And adding to that, every time this function is called, I would like to start at the index where the last call ended. So, moving on from the last example, the next time this is called:

runs = 1
looper(['A', 'B', 'C'], runs)

Then the output would be:

c

Having not yet quite wrapped my head around the use of generators, I can only guess that this would be a useful approach. But I've picked up that you should only use a generator if you'd like to loop through a list once as per the top answer in the post Generator Expressions vs. List Comprehension. So as far as I know, a generator would not be useful after all since Resetting a generator object in Python seems to be a bit complicated. Thank you for any suggestions!

vestland
  • 55,229
  • 37
  • 187
  • 305
  • 5
    I think https://docs.python.org/3.8/library/itertools.html#itertools.cycle is what you want – bb1950328 Aug 27 '20 at 07:51
  • Or use `lst[i % len(lst)]`. Dup: https://stackoverflow.com/questions/23416381/circular-list-iterator-in-python – Tomerikoo Aug 27 '20 at 07:55
  • 4
    BTW, generator is **Exactly** what you want here, as you can pass the same generator object between different calls to your function and this way you "*remeber*" the last item yielded and keep the cyclic looping even between calls – Tomerikoo Aug 27 '20 at 08:13
  • @Tomerikoo Sounds great! Would you care to share an answer here describing that in detail with the provided example? If not, I'll just go ahead and let the dupe hammer fall. – vestland Aug 27 '20 at 08:29

2 Answers2

5

Combining the suggestions in the comments you can achieve the exact behavior you're looking for. I will not go into details as to how generators work, as you have much more concise answers from experienced people. I will though sum-up that the point with generators is that they yield for you the next element only when you ask for it. And generators are objects just as anything else in Python, so as long as you are sharing the same generator object you will always have a bookmark of the last element yielded. Now to better understand with code:

First we will generalize the looper function:

def looper(iterable, runs):
    i = cycle(iterable)
    for _ in range(runs):
        print(next(i))
    return i

So now it allows you to call the function once with as many runs you would like, on any iterable. On the other hand, if you intend to call it multiple times, it returns the generator created for re-use.

Now let's test some cases:

a = [1, 2, 3]

looper(a, 6)

This will print:

1
2
3
1
2
3

But more importantly, this will also:

i = looper(a, 2)
looper(i, 2)
looper(i, 2)

Notes:

  • We are able to do all this because of the fact that itertools.cycle is returning a generator object.

  • If you want the cyclic effect between calls, you must pass the returned generator to subsequent calls. If we were trying to do for example looper(a, 2) a few times we would always get:

      1
      2
    

    This is because every time we will call the function, we are calling cycle again and creating a new generator from the original list that starts from the first element.

  • On the other hand, when passing a generator to the function, the cycle function will create a new generator, but from the point the previous one stopped. This way if you keep passing the first returned cycle generator you preserve the cyclic effect.

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
1

You present two questions here. The first one which conform to your question heading is how to (print) cyclic content of a list (you use ‘looper’ here), and not to break the code with high number of runs as provided in your code.

I test your code, and indeed for say, runs = 13, your code break with error code IndexError: list index out of range.

I see your requirement more of better algorithm, so I use python floor division operator (//) for cycle (or frequency) and python reminder division (%) as reminder.

As we know mathematically, say 5//2 = 2 and 5%2 = 1

The algorithm here is to print all the list content through the calculated cycle accordingly, and then print the rest reminder. Note in python by default index start with 0.

Here my proposed code

def looper(lst, runs):
    cycle = (runs-1)//len(lst)
    reminder = (runs-1)%len(lst)
    print('cycle', cycle)
    print('reminder', reminder)
    for i in range(cycle):
        for j in lst:
            print(j)
    for index, k in enumerate(lst):
        if index <= reminder:
            print(k)

# runs must be any positive integer
runs = 13
looper(['A', 'B', 'C'], runs)

I have test it with runs = 13, the code will not break print the following, as expected

cycle 4 reminder 0 A B C A B C A B C A B C A

The second question (or requirement) is whenever this code is running, the starting index is not from 0 but from last reminder (we can think it as pointer), which I think is another subject. But I may come back here to give that solution as well. Cheers.

KokoEfraim
  • 198
  • 8