Previous in yield operations - python

Viewed 474

Recently i have been using the 'yield' in python. And I find generator functions very useful. My query is that, is there something which could decrement the imaginative cursor in the generator object. Just how next(genfun) moves and outputs +i'th item in the container, i would like to know if there exists any function that may call upon something like previous(genfun) and moves to -1th item in the conatiner.

Actual Working

def wordbyword():
  words = ["a","b","c","d","e"]
  for word in words:
    yield word

getword = wordbyword()

next(getword)
next(getword)

Output's

a
b

What I would like to see and achieve is

def wordbyword():
  words = ["a","b","c","d","e"]
  for word in words:
    yield word

getword = wordbyword()

next(getword)
next(getword)
previous(getword)

Expected Output

a
b
a

This may sound silly, but is there someway there is this previous in generator, if not why is it so?. Why not we could decrement the iterator, or am I ignorant of an existing method, pls shower some light. What can be the closest way to implement what I have here in hand.

4 Answers

No there is no such function to sort of go back in a generator function. The reason is that Python does not store up the previous value in a generator function natively, and as it does not store it, it also cannot perform a recalculation.

For example, if your generator is a time-sensitive function, such as

def time_sensitive_generator():
    yield datetime.now()

You will have no way to recalculate the previous value in this generator function.

Of course, this is only one of the many possible cases that a previous value cannot be calculated, but that is the idea.

If you do not store the value yourself, it will be lost forever.

As already said, there is no such function since the entire point of a generator is to have a small memory footprint. You would need to store the result.

You could automate the storing of previous results. One use-case of generators is when you have a conceptually infinite list (e.g. that of prime numbers) for which you only need an initial segment. You could write a generator that builds up these initial segments as a side effect. Have an optional history parameter that the generator appends to while it is yielding. For example:

def wordbyword(history = None):
  words = ["a","b","c","d","e"]
  for word in words:
      if isinstance(history,list): history.append(word)
      yield word

If you use the generator without an argument, getword = wordbyword(), it will work like an ordinary generator, but if you pass it a list, that list will store the growing history:

hist = []
getword = wordbyword(hist)

print(next(getword)) #a
print(next(getword)) #b
print(hist) #['a','b']

Iterating over a generator object consumes its elements, so there is nothing to go back to after using next. You could convert the generator to a list and implement your own next and previous

index = 0


def next(lst):
    global index
    index += 1
    if index > len(lst):
        raise StopIteration
    return lst[index - 1]


def previous(lst):
    global index
    index -= 1
    if index == 0:
        raise StopIteration
    return lst[index - 1]

getword = list(wordbyword())
print(next(getword)) # a
print(next(getword)) # b
print(previous(getword)) # a

One option is to wrap wordbyword with a class that has a custom __next__ method. In this way, you can still use the built-in next function to consume the generator on-demand, but the class will store all the past results from the next calls and make them accessible via a previous attribute:

class save_last:
   def __init__(self, f_gen):
      self.f_gen = f_gen
      self._previous = []
   def __next__(self):
      self._previous.append(n:=next(self.i_gen))
      return n
   def __call__(self, *args, **kwargs):
      self.i_gen = self.f_gen(*args, **kwargs)
      return self
   @property
   def previous(self):
      if len(self._previous) < 2:
         raise Exception
      return self._previous[-2] 

@save_last
def wordbyword():
   words = ["a","b","c","d","e"]
   for word in words:
      yield word

getword = wordbyword()
print(next(getword))
print(next(getword))
print(getword.previous)

Output:

a
b
a
Related