Home > Software design >  How to start Itertools cycle from a particular point in Python?
How to start Itertools cycle from a particular point in Python?

Time:04-23

I have created an itertools cycle for the English alphabet using the code below,

lowercase_letters_cycle = itertools.cycle(string.ascii_lowercase)

If I run a for loop on this iterator object, the first iteration would give me "a" as the output because the cycle starts from "a". How can I make it so that cycle starts from any letter of my choice?

One way that works is,

def start_cycle(letter):
  lowercase_letters_cycle = itertools.cycle(lowercase_letters)
  letter_index = lowercase_letters.index(letter)
  index = 0

  while True:
    if index == letter_index:
      break

    letter = next(lowercase_letters_cycle)
    index  = 1

  return lowercase_letters_cycle

But is there any shorter method?

CodePudding user response:

The itertools documentation supplies a recipe for consuming a number of items from an iterator.

from itertools import islice
import collections


def consume(iterator, n=None):
    "Advance the iterator n-steps ahead. If n is None, consume entirely."
    # Use functions that consume iterators at C speed.
    if n is None:
        # feed the entire iterator into a zero-length deque
        collections.deque(iterator, maxlen=0)
    else:
        # advance to the empty slice starting at position n
        next(islice(iterator, n, n), None)

So you create the cycle, then just consume a bit of it before proceeding.

lowercase_letters_cycle = itertools.cycle(string.ascii_lowercase)

consume(lowercase_letters_cycle, ord('n') - ord('a'))

assert next(lowercase_letters_cycle) == 'n')

The same consume is also available from the third-party more-itertools package.

CodePudding user response:

You can combine islice and cycle from the itertools module as follows:

import string 
import itertools

my_it = itertools.islice(itertools.cycle(string.ascii_lowercase), 3, None)

It will yield d (the character that comes after the first 3), then e, ...., then z, then a, then b, and so on. You can change the number in the second argument to islice to start from a different letter.

CodePudding user response:

You could just slice the input list to start at the index of your choice.

offset = 10
lowercase_letters = list(string.ascii_lowercase)

#                 offset_index to end         start to offset_index-1
offset_letters = lowercase_letters[offset:]   lowercase_letters[:offset]
offset_letters_cycle = itertools.cycle(offset_letters)

Then,

for i in range(10):
    print(next(offset_letters_cycle), end=" ")

prints:

k l m n o p q r s t 

CodePudding user response:


import string
from itertools import cycle, dropwhile

def start_cycle(letter):
    return dropwhile(lambda x: x!= letter, cycle(string.ascii_lowercase))

itertools.dropwhile will feed the iterable results to its sentinel function - in this case lambda x: x != letter, and will "swallow" the results until the function returns False for the first time. At that point the function is no longer called, and the iterable proceeds, yielding any further values.

Also, note there is no need to convert ascii_lowercase to a list, as strings are already iterables.

Keep in mind that this will run into an infinite loop with 100% CPU if a character not in ascii_lowercase is passed. It is better to guard it with a check:


def start_cycle(letter):
    if letter not in string.ascii_lowercase:
         raise ValueError()
    return dropwhile(lambda x: x!= letter, cycle(string.ascii_lowercase))

  • Related