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