Home > database >  How can I make a non-random number generator?
How can I make a non-random number generator?

Time:03-29

I'd like to construct an object that works like a random number generator, but generates numbers in a specified sequence.

# a random number generator
rng = lambda : np.random.randint(2,20)//2

# a non-random number generator
def nrng():
    numbers = np.arange(1,10.5,0.5)
    for i in range(len(numbers)):
        yield numbers[i]

for j in range(10):
    print('random number', rng())
    print('non-random number', nrng())

The issue with the code above that I cannot call nrng in the last line because it is a generator. I know that the most straightforward way to rewrite the code above is to simply loop over the non-random numbers instead of defining the generator. I would prefer getting the example above to work because I am working with a large chunk of code that include a function that accepts a random number generator as an argument, and I would like to add the functionality to pass non-random number sequences without rewriting the entire code.

CodePudding user response:

You can call next() with a generator or iterator as an argument to withdraw exactly one element from it. Saving the generator to a variable beforehand allows you to do this multiple times.

# make it a generator
def _rng():
    while True:
        yield np.random.randint(2,20)//2

# a non-random number generator
def _nrng():
    numbers = np.arange(1,10.5,0.5)
    for i in range(len(numbers)):
        yield numbers[i]

rng = _rng()
nrng = _nrng()
for j in range(10):
    print('random number', next(rng))
    print('non-random number', next(nrng))

CodePudding user response:

np.random.randint can remember the last number it generated because it's a function of the np.random.RandomState class. Numpy simply aliases the class method so that it's accessible directly from the np.random module instead of having you access it through the class.

Knowing this, you can write your own class to work like so:

class NotRandom:

    numbers = np.arange(1,10.5,0.5)
    last_index = -1

    @classmethod
    def nrng(cls):
        cls.last_index  = 1
        if cls.last_index < len(cls.numbers):
            return cls.numbers[cls.last_index]
        # else:
        return None

# Create an alias to the classmethod
nrng = NotRandom.nrng      # Note this is OUTSIDE the class

Then, you can do:

print(nrng())    # 1.0
print(nrng())    # 1.5
print(nrng())    # 2.0

If you want to be able to have multiple concurrent instances of nrng, you could make nrng() an instance method instead of a class method:

class NotRandom:
    def __init__(self):
        self.numbers = np.arange(1,10.5,0.5)
        self.last_index = -1

    def nrng(self):
        self.last_index  = 1
        if self.last_index < len(self.numbers):
            return self.numbers[self.last_index]
        # else:
        return None

# Create an object. Then create an alias to its method
nrng = NotRandom().nrng 

Then, you can use nrng() to refer to the method that's bound to the NotRandom instance you created. If you want another instance, you can have that too:

another_notrandom = NotRandom()
nrng2 = another_notrandom.rng

print(nrng())    # 1.0
print(nrng())    # 1.5

print(nrng2())   # 1.0

print(nrng())    # 2.0

print(nrng2())   # 1.5

CodePudding user response:

function that accepts a random number generator as an argument

Call it like this then:

that_function(nrng().__next__)

Or with functools.partial:

that_function(partial(next, nrng()))

Or without your generator, if that arange is all you want:

that_function(iter(np.arange(1,10.5,0.5)).__next__)

Demo code:

import numpy as np
from functools import partial

# a random number generator
rng = lambda : np.random.randint(2,20)//2

# a non-random number generator
def nrng():
    numbers = np.arange(1,10.5,0.5)
    for i in range(len(numbers)):
        yield numbers[i]

def that_function(rng):
    print(*(rng() for j in range(10)))

that_function(rng)
that_function(nrng().__next__)
that_function(iter(np.arange(1,10.5,0.5)).__next__)
that_function(partial(next, nrng()))

Output (Try it online!):

4 6 1 3 8 7 3 6 2 1
1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5
1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5
1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5

CodePudding user response:

If you want your object to keep state and look like a function, create a custom class with __call__ method.

eg.

class NRNG:
    def __init__(self):
        self.numbers = range(10)
        self.state = -1
    def __call__(self):
        self.state  = 1
        return self.numbers[self.state]
        
nrng = NRNG()


for i in range(10):
    print(nrng())

However, I wouldn't recommend this unless absolutely necessary, as it obscures the fact that your nrng keeps a state (although technically, most rngs keep their state internally).

It's best to just use a regular generator with yield by calling next on it or to write a custom iterator (also class-based). Those will work with things like for loops and other python tools for iteration (like the excellent itertools package).

  • Related