random_cycler.py 1.3 KB

12345678910111213141516171819202122232425262728293031323334353637
  1. import random
  2. class RandomCycler:
  3. """
  4. Creates an internal copy of a sequence and allows access to its items in a constrained random
  5. order. For a source sequence of n items and one or several consecutive queries of a total
  6. of m items, the following guarantees hold (one implies the other):
  7. - Each item will be returned between m // n and ((m - 1) // n) + 1 times.
  8. - Between two appearances of the same item, there may be at most 2 * (n - 1) other items.
  9. """
  10. def __init__(self, source):
  11. if len(source) == 0:
  12. raise Exception("Can't create RandomCycler from an empty collection")
  13. self.all_items = list(source)
  14. self.next_items = []
  15. def sample(self, count: int):
  16. shuffle = lambda l: random.sample(l, len(l))
  17. out = []
  18. while count > 0:
  19. if count >= len(self.all_items):
  20. out.extend(shuffle(list(self.all_items)))
  21. count -= len(self.all_items)
  22. continue
  23. n = min(count, len(self.next_items))
  24. out.extend(self.next_items[:n])
  25. count -= n
  26. self.next_items = self.next_items[n:]
  27. if len(self.next_items) == 0:
  28. self.next_items = shuffle(list(self.all_items))
  29. return out
  30. def __next__(self):
  31. return self.sample(1)[0]