-1

Is there a way to transform cycle from itertools into list? Applying list(my_cycle) freezes my computer.

I would like to periodically switch between a collection of objects infinitely. They are stored in a cycle. If one of my objects become 'inactive' I would like to delete it from cycle. I solved it with another list with inactive objects but it looks like bad workaround.

wim
  • 338,267
  • 99
  • 616
  • 750
danielleontiev
  • 869
  • 6
  • 26
  • `Applying list() freezes my computer.` of course it does. `cycle` never raises `StopIteration`. Even if it did, how would that list look like? – DeepSpace Sep 17 '18 at 18:22
  • What do you expect to be the result? `cycle` represents an infinite number of items repeating over and over. – mkrieger1 Sep 17 '18 at 18:23
  • You can do it if you have infinite RAM in your computer ;) – wim Sep 17 '18 at 18:23
  • 1
    I suppose its internal design is built on `list`. So it is easy to convert it to list – danielleontiev Sep 17 '18 at 18:23
  • 2
    @danielleontiev What lead you to assume that? – DeepSpace Sep 17 '18 at 18:24
  • @DeepSpace because you are usually create cycles from lists in python and because I would implement it this way if I should :) – danielleontiev Sep 17 '18 at 18:26
  • Do you have a specific problem you are trying to solve? Maybe there is an entirely different approach. – mkrieger1 Sep 17 '18 at 18:26
  • @mkrieger1 I would like to delete element from cycle – danielleontiev Sep 17 '18 at 18:27
  • Can you show an example? – mkrieger1 Sep 17 '18 at 18:28
  • @mkrieger1 I would like to periodically switch between accounts infinetely. They are stored in cycle. If one of my accounts become inactive i would like to delete it from cycle. I solved it with another list with inactive accounts but it looks like bad workaround – danielleontiev Sep 17 '18 at 18:30
  • 1
    @danielleontiev: You might want to ask a (new) question about that actual problem. You've got [an XY problem](https://meta.stackexchange.com/q/66377/322040) here, where you're trying to get us to help make a poor approach work, when the real solution is to find a better approach. – ShadowRanger Sep 17 '18 at 18:32
  • @ShadowRanger yes maybe I should learn how to ask questions – danielleontiev Sep 17 '18 at 18:35
  • @danielleontiev: As it happens, `cycle` is implemented using an internal `list` that caches the items on the first run through (your intuition there isn't wrong). But that `list` isn't exposed to API users; if you want to omit an element from future cycles, you need to recreate the `cycle` object and start using the new object. – ShadowRanger Sep 17 '18 at 18:35
  • @ShadowRanger thanks, maybe It is the best approach – danielleontiev Sep 17 '18 at 18:38
  • @danielleontiev: If you do ask a new question, you might want to include info on how often elements become inactive, whether they ever become active again and if so how often, whether new objects (not previously active or inactive) ever appear, etc. If switching states is common, but the set of all object (inactive + active) is constant, your existing solution changed from a `list` to a `set` combines in a not completely unreasonable way to allow the use of a single `cycle` where you just live filter it as you go, using a filter condition based on the `set` so it's cheap to evaluate. – ShadowRanger Sep 17 '18 at 18:57
  • @ShadowRanger do you think it is necessary to ask another question or not? I think it is enough information here in comments or it's better to share with community on such problems and ask the question? – danielleontiev Sep 17 '18 at 19:01
  • @danielleontiev: If you've got a more specific problem you can provide a [MCVE] for, I'd ask a new question focused on solving your real problem. – ShadowRanger Sep 17 '18 at 19:06
  • @ShadowRanger thanks a lot. I think it's enough for this problem. Next time I will formulate my questions according to your example – danielleontiev Sep 17 '18 at 19:10

2 Answers2

1

No, you can not, because a cycle is an infinite sequence. Your computer "freezes" because Python is trying to iterate a never-ending collection of items (if you left it long enough, the process would run out of memory and crash).

What you can do, is collect a predetermined finite amount of the items into a list:

n = 10  # some fixed size 
results = []
for i in range(n):
    results.append(next(my_cycle))

There is no general way to know how many items to consume to get one pass through a cycle, because the cycle object does not expose any state about the period of the underlying iteration, i.e. how many items were iterated before repeating.

There is no public way to modify the items returned from a cycle once the first StopIteration from the original iterator has been encountered, they are all buffered to private memory somewhere:

>>> L = [0,1,2]
>>> g = itertools.cycle(L)
>>> next(g)
0
>>> L.remove(1)
>>> next(g)
2
>>> next(g)
0
>>> L.remove(2)
>>> next(g)
2

For cycling a mutable sequence, as an alternative design choice you could consider using a collections.deque instance as your data structure (the rotate method is efficient).

wim
  • 338,267
  • 99
  • 616
  • 750
  • For reference: https://stackoverflow.com/questions/4152376/how-to-get-the-n-next-values-of-a-generator-in-a-list-python – mkrieger1 Sep 17 '18 at 18:29
  • @DeepSpace: Probably meant to use `itertools.islice`? – ShadowRanger Sep 17 '18 at 18:30
  • 1
    "There is no public way to modify the items returned from a cycle once the first `StopIteration` from the original iterator has been encountered." Adding to that, even before the original iterator has been exhausted, any code that tried to modify the cycle by modifying whatever the original iterator was based on would be a maintenance nightmare waiting to happen, and likely break various rules (implicit or explicit) of Python (e.g. don't modify collections while iterating them). – ShadowRanger Sep 17 '18 at 18:38
  • It's important to understand that this will not work after `cycle` iterated over the entire iterable once. See this [gist](https://gist.github.com/DeepSpace2/d4043b59bc2c417620175716adacdbd8) This is because `cycle` uses an internal structure to store "visited" elements which we don't have access to. – DeepSpace Sep 17 '18 at 18:42
  • 1
    Yes, implementing my own cycle or using another data structure like deque are the best choices to solve this problem – danielleontiev Sep 17 '18 at 18:43
1

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:

  1. It makes it cheap to activate and deactivate objects
  2. 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
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271