DEV Community

Cover image for How computers can turn randomness into meaningful numbers
Tim McNamara
Tim McNamara

Posted on

How computers can turn randomness into meaningful numbers

One of the things that really intrigued me when I started to program was that I could ask the computer: "Please generate a random number between 50 and 200".

The thing that bugged me was that random number generators seemed to produce numbers between 0 and 1. What's involved in generating that value between 0 and 1? And how does that become a number between 50 and 200?

Today I decided to implement from scratch in case anyone else has a simple mind like me.

The problem

Let's say that we're presented with a random number generator that only produces random bytes.. but what we really want is a random number between [n,m].

The solution

  • Treat your incoming random bytes as an unsigned integer, then divide by the max that integer could generate. We now have a proportion between 0 and "max integer".
  • Multiply the difference between the two numbers in the range by the proportion
  • Add start to the result of the previous operation

The easy implementation

First, let's assume that can get the proportion for free by calling random.random().

import random

def rand_range(start, end):
    diff = end - start
    proportion = random.random()
    return start + (proportion * diff)

The hard implementation

When we need to generate the proportion ourselves, there is more work to do. An explanation follows below!

def random_bytes(n_bytes, source='/dev/urandom'):
    with open(source, 'rb') as f:
        entropy = f.read(n_bytes)
    return entropy

def rand_from_bytes(n_bytes):
    assert n_bytes in {1, 2, 4, 8}

    entropy = random_bytes(n_bytes)
    typecode = {1: 'B', 2: 'H', 4: 'I', 8: 'Q'}[n_bytes]
    r, = struct.unpack(typecode, entropy)
    return r / (1 << 8*n_bytes)

def rand_range(start, end):
    diff = end - start
    proportion = rand_from_bytes(4)
    return start + (proportion * diff)

In random_bytes() we read a fixed number of random bytes. By "random bytes", I mean an unpredictable sequence of 0s and 1s. On UNIXy operating systems, we can read from the pseudo-file /dev/urandom to get a sequence. (The operating kernel makes sure that the sequence really is unpredictable.) Those bytes are passed through to rand_from_bytes().

rand_from_bytes() uses lots of quirky syntax and some trickery. It uses Python's struct module to interpret the bytes as unsigned integers using Python's rules for doing so. That module requires a coded string to describe how the bytes provided in the second argument should be interpreted. The code {1: 'B', 2: 'H', 4: 'I', 8: 'Q'}[n_bytes] creates a Python dictionary that is immediately accessed to generate that coded string.

r / (1 << 8*n_bytes) might look like black magic, but all it's doing is generating a proportion for us. (1 << 8*n_bytes) is the maximum integer value for whatever type that we care about. We use that as a denominator. r lies somewhere between 0 and (1 << 8*n_bytes).

Python will return a floating point number because of the division.

And now we're back where we started with when calling random.random(). Yay. And then we repeat the easy implementation.

Questions?

If you've not read much Python before—or even if you have and you haven't used the struct module before—code like this can seem kind of crazy. Hopefully not too crazy. If you have any questions, please ask!

Image Credit

Dylan Nolte / Unsplash

Discussion (0)