Search code examples
pythonpython-3.xgenerator

How can I make an object with an interface like a random number generator, but that actually generates a specified sequence?


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.

EDIT: I see some confusion in the comments. I am aware that python's random number generators generate pseudo-random numbers. This post is about replacing a pseudo-random-number generator by a number generator that generates numbers from a non-random, user-specified sequence (e.g., a generator that generates the number sequence 1,1,2,2,1,0,1 if I want it to).


Solution

  • Edit:

    The cleanest way to do this would be to use a lambda to wrap your call to next(nrng) as per great comment from @GACy20:

    def nrng_gen():
        yield from range(10)
    
    nrng = nrng_gen()
    
    nrng_func = lambda: next(nrng)
    
    for i in range(10):
        print(nrng_func())
    

    Original answer:

    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).