Search code examples
pythonpython-itertoolscycle

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

  • If the player IDs are unique, you can maintain a set of elements to skip while using itertools.cycle():

    import random
    from itertools import cycle
    
    players = [1, 2, 3]
    players_to_skip = set()
    player_iterator = cycle(players)
    
    while len(players_to_skip) < len(players):
        current_player = next(player_iterator)
        if current_player in players_to_skip:
            continue
    
        print(f"Player {current_player}'s turn")
        if random.randint(0, 6) == 1:
            players_to_skip.add(current_player)