Python's random module is actually built around the random.Random type. Various "random functions" such as random.random merely use a default instance of that type.
To create your own random number generator, it is sufficient to subclass random.Random and replace its random method. While not strictly required for basic functionality, it is highly advisable to adjust the seed, getstate and setstate methods as well.
class RangeRandom(Random):
"""
Example "random" number generator that provides numbers from a uniform sequence
"""
def __init__(self, resolution=100):
self._pos = 0
self._resolution = resolution
def random(self) -> float:
self._pos = (self._pos + 1) % self._resolution
return self._pos / self._resolution
def getstate(self):
return self._pos, self._resolution
def setstate(self, state) -> None:
self._pos,self._resolution = state
def seed(self, a) -> None:
self._pos = int(a) % self._resolution
Notably, defining the random method is sufficient to make all other methods use the same underlying "randomness". For example, random integers as used by choice or randint will be drawn based on the random method:
>>> rand = RangeRandom()
>>> vals = [1, 2, 3, 4]
>>> [rand.choice([1, 2, 3, 4]) for _ in range(5)]
[2, 4, 2, 4, 2, 4]
It is worth pointing out that while implementing random is sufficient, random.Random then uses an internal helper to convert the random floats to random integers; this is inherently lossy, as float has limited precision. If the random number generator permits it, it is advisable to also implement _randbelow or at least getrandbits.
Returns a non-negative Python integer with k random bits. This method is supplied with the MersenneTwister generator and some other generators may also provide it as an optional part of the API. When available, getrandbits() enables randrange() to handle arbitrarily large ranges.
The undocumented method _randbelow must take an integer n and return a random integer in the interval [0, n).
class ArbitraryRangeRandom(RangeRandom):
"""
Example "random" number generator for floats/ints from a uniform sequence
"""
def _randbelow(self, n: int) -> int:
self.random() # advance the RNG
if n == 0:
return 0
elif n <= self._resolution:
return self._pos % n
else:
return self._pos * (n // self._resolution)
>>> rand = RangeRandom()
>>> vals = [1, 2, 3, 4]
>>> # explicit int support provides better resolution/spacing
>>> print([rand.choice([1, 2, 3, 4]) for _ in range(5)])
[2, 3, 4, 1, 2]