Home > Enterprise >  How to cycle over a Python list while removing items until there are none left
How to cycle over a Python list while removing items until there are none left

Time:02-21

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.

CodePudding user response:

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)

CodePudding user response:

You could use pop() to consume list items.

players = [1, 2, 3]

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

CodePudding user response:

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

CodePudding user response:

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)
  • Related