If your set of all object (active and inactive) never changes, and especially if transitions between active and inactive state are common, the total number of objects isn't outrageous or the set of inactive objects doesn't usually cover most of the total set, cycle
would still work fairly well here, by keeping a set
of inactive objects around and filtering out currently inactive objects "live":
from itertools import cycle, filterfalse
allobjects = [...]
numuniqueobjects = len(set(allobjects))
inactiveobjects = set()
# Each time we request an item, filterfalse pulls items from the cycle
# until we find one that isn't in our inactive set
for object in filterfalse(inactiveobjects.__contains__, cycle(allobjects)):
# ... do actual stuff with object ...
# Any objects that should go active again get removed from the set and will be
# seen again the next time their turn comes up in the original order
inactiveobjects -= objects_that_should_become_active()
# Won't see this object again until it's removed from inactiveobjects
if object.should_go_inactive():
inactiveobjects.add(object)
if len(inactiveobjects) == numuniqueobjects:
# Nothing is active, continuing loop would cause infinite loop
break
Advantages to this design are that:
- It makes it cheap to activate and deactivate objects
- It preserves original iteration order indefinitely (if an object was originally in position 4, when it goes inactive it is skipped, and when it goes active again, it reappears after position 3 and before position 5 the next time you cycle through)
The main downside is that it adds a tiny bit more overhead to the "nothing is changing" case, especially if the set
of inactiveobjects
grows to an appreciable fraction of the total number of objects; you still have to cycle
all objects even if filter out many of them.
If that doesn't suit your use case, a custom version of cycle
built on a deque
as suggested by wim is probably the best general purpose solution:
from collections import deque
from collections.abc import Iterator
class mutablecycle(Iterator):
def __init__(self, it):
self.objects = deque(it)
self.objects.reverse() # rotate defaults to equivalent of appendleft(pop())
# reverse so next item always at index -1
def __next__(self):
self.objects.rotate() # Moves rightmost element to index 0 efficiently
try:
return self.objects[0]
except IndexError:
raise StopIteration
def removecurrent(self):
# Remove last yielded element
del self.objects[0]
def remove(self, obj):
self.objects.remove(obj)
def add(self, obj, *, tofront=True):
if tofront:
# Putting it on right makes it be yielded on next request
self.objects.append(obj)
else:
# Putting it on left makes it appear after all other elements
self.objects.appendleft(obj)
Use would be:
mycycle = mutablecycle(allobjects):
for object in mycycle:
# ... do stuff with object ...
if object.should_go_inactive():
mycycle.removecurrent() # Implicitly removes object currently being iterated