code: def itershuffle(iterable, bufsize=1000):
      
      iterable = iter(iterable)
      buf = []
      try:
          while True:
              for i in range(random.randint(1, bufsize - len(buf))):
                  buf.append(next(iterable))
              random.shuffle(buf)
              for i in range(random.randint(1, bufsize)):
                  if buf:
                      yield buf.pop()
                  else:
                      break
      except StopIteration:
          random.shuffle(buf)
          while buf:
              yield buf.pop()
          raise StopIteration
docs: Shuffle an iterator. This works by holding `bufsize` items back
      and yielding them sometime later. Obviously, this is not unbiased –
      but should be good enough for batching. Larger bufsize means less bias.
      From https://gist.github.com/andres-erbsen/1307752
  
      iterable (iterable): Iterator to shuffle.
      bufsize (int): Items to hold back.
      YIELDS (iterable): The shuffled iterator.