How to override Python's random with own generator?

Viewed 250

In the docs for Python's random, it states that one can "use a basic generator of your own" by overriding the random() method. Suppose I have a custom generator that outputs a random float uniformly in the semi-open range [0.0, 1.0), i.e. just like random().

  • How would I go about replacing Python's with my own?

Further, suppose the replacement is a true random generator built off of a natural source of entropy.

  • Does this mean I can use the other parts of the class like random.choice() on a given array and make claims that the chosen element is truly random?
1 Answers

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.

random.getrandbits(k)

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]
Related