'How to cycle over a Python list while removing items until there are none left

The cycle iterator in the standard library does not have insert or remove methods, so you can't modify the values once it is instantiated:

from itertools import cycle
import random

players = cycle([1, 2, 3])
while len(players) > 0:
    player = next(player)
    print(f"Player {player}'s turn")
    if random.randint(0, 6) == 1:
        players.remove(player)

# Raises TypeError: 'object of type 'itertools.cycle' has no len()'

This isn't surprising, since it iterates over the list argument, storing each item as it goes and doesn't 'know' the full contents of the list until it has completed the first cycle.

Is there an alternative which would achieve the following behaviour:

players = [1, 2, 3]
i = 0
while len(players) > 0:
    i = i % len(players)
    player = players[i]
    print(f"Player {player}'s turn")
    if random.randint(0, 6) == 1:
        players.remove(player)
    else:
        i += 1

I realize this is works, but I'm wondering if I'm missing a simpler approach.

I considered ways to re-build the cycle every time after removing an item but this would also reset the position to the beginning of the cycle:

from itertools import cycle

players = [1, 2, 3]
while len(players) > 0:
    for player in cycle(players):  # Problem: always starts with player 1
        print(f"Player {player}'s turn")
        if random.randint(0, 6) == 1:
            players.remove(player)
            break

But, I couldn't figure out an easy way to move to the next player after removing an element.



Solution 1:[1]

You could use pop() to consume list items.

players = [1, 2, 3]

while players:
    player = players.pop()
    print('player:', player)

Solution 2:[2]

Here's one way round the problem. It removes the player and then re-orders the list so the next player comes next.

from itertools import cycle

players = [1, 2, 3]
while len(players) > 0:
    for player in cycle(players):
        print(f"Player {player}'s turn")
        if random.randint(0, 6) == 1:
            i = players.index(player)
            # Remove player and re-order list
            players = players[i+1:] + players[:i]
            break

Or, similar thing using pop:

from itertools import cycle, islice

players = [1, 2, 3]
i = 0
while len(players) > 0:
    for player in islice(cycle(players), i, None):
        print(f"Player {player}'s turn")
        if random.randint(0, 6) == 1:
            i = players.index(player)
            # Remove player
            players.pop(i)
            break

Solution 3:[3]

Having consider the options so far, I think it might be easier to forget about cycle and just nest a for loop in a while loop, while remembering to make a copy of the for loop iterable so that it does not get modified. Provided you only remove the current player each iteration this should work:

players = [1, 2, 3]
while len(players) > 0:
    for player in players.copy():
        print(f"Player {player}'s turn")
        if random.randint(0, 6) == 1:
            players.remove(player)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Cole Tierney
Solution 2
Solution 3 Bill