Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2f726e9

Browse files
committed
SF bug #812202: randint is always even
* Added C coded getrandbits(k) method that runs in linear time. * Call the new method from randrange() for ranges >= 2**53. * Adds a warning for generators not defining getrandbits() whenever they have a call to randrange() with too large of a population.
1 parent 5c68ef0 commit 2f726e9

5 files changed

Lines changed: 196 additions & 10 deletions

File tree

Doc/lib/librandom.tex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ \section{\module{random} ---
4141
different basic generator of your own devising: in that case, override
4242
the \method{random()}, \method{seed()}, \method{getstate()},
4343
\method{setstate()} and \method{jumpahead()} methods.
44+
Optionally, a new generator can supply a \method{getrandombits()}
45+
method --- this allows \method{randrange()} to produce selections
46+
over an arbitrarily large range.
47+
\versionadded[the \method{getrandombits()} method]{2.4}
4448

4549
As an example of subclassing, the \module{random} module provides
4650
the \class{WichmannHill} class which implements an alternative generator
@@ -92,6 +96,14 @@ \section{\module{random} ---
9296
separated by many steps.]{2.3}
9397
\end{funcdesc}
9498

99+
\begin{funcdesc}{getrandbits}{k}
100+
Returns a python \class{long} int with \var{k} random bits.
101+
This method is supplied with the MersenneTwister generator and some
102+
other generators may also provide it as an optional part of the API.
103+
When available, \method{getrandbits()} enables \method{randrange()}
104+
to handle arbitrarily large ranges.
105+
\versionadded{2.4}
106+
\end{funcdesc}
95107

96108
Functions for integers:
97109

Lib/random.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
4040
"""
4141

42+
from warnings import warn as _warn
43+
from types import MethodType as _MethodType, BuiltinMethodType as _BuiltinMethodType
4244
from math import log as _log, exp as _exp, pi as _pi, e as _e
4345
from math import sqrt as _sqrt, acos as _acos, cos as _cos, sin as _sin
4446
from math import floor as _floor
@@ -47,12 +49,14 @@
4749
"randrange","shuffle","normalvariate","lognormvariate",
4850
"expovariate","vonmisesvariate","gammavariate",
4951
"gauss","betavariate","paretovariate","weibullvariate",
50-
"getstate","setstate","jumpahead"]
52+
"getstate","setstate","jumpahead", "WichmannHill", "getrandbits",
53+
"Random"]
5154

5255
NV_MAGICCONST = 4 * _exp(-0.5)/_sqrt(2.0)
5356
TWOPI = 2.0*_pi
5457
LOG4 = _log(4.0)
5558
SG_MAGICCONST = 1.0 + _log(4.5)
59+
BPF = 53 # Number of bits in a float
5660

5761
# Translated by Guido van Rossum from C source provided by
5862
# Adrian Baddeley. Adapted by Raymond Hettinger for use with
@@ -72,6 +76,8 @@ class Random(_random.Random):
7276
Class Random can also be subclassed if you want to use a different basic
7377
generator of your own devising: in that case, override the following
7478
methods: random(), seed(), getstate(), setstate() and jumpahead().
79+
Optionally, implement a getrandombits() method so that randrange()
80+
can cover arbitrarily large ranges.
7581
7682
"""
7783

@@ -131,12 +137,13 @@ def __reduce__(self):
131137

132138
## -------------------- integer methods -------------------
133139

134-
def randrange(self, start, stop=None, step=1, int=int, default=None):
140+
def randrange(self, start, stop=None, step=1, int=int, default=None,
141+
maxwidth=1L<<BPF):
135142
"""Choose a random item from range(start, stop[, step]).
136143
137144
This fixes the problem with randint() which includes the
138145
endpoint; in Python this is usually not what you want.
139-
Do not supply the 'int' and 'default' arguments.
146+
Do not supply the 'int', 'default', and 'maxwidth' arguments.
140147
"""
141148

142149
# This code is a bit messy to make it fast for the
@@ -146,43 +153,52 @@ def randrange(self, start, stop=None, step=1, int=int, default=None):
146153
raise ValueError, "non-integer arg 1 for randrange()"
147154
if stop is default:
148155
if istart > 0:
156+
if istart >= maxwidth:
157+
return self._randbelow(istart)
149158
return int(self.random() * istart)
150159
raise ValueError, "empty range for randrange()"
151160

152161
# stop argument supplied.
153162
istop = int(stop)
154163
if istop != stop:
155164
raise ValueError, "non-integer stop for randrange()"
156-
if step == 1 and istart < istop:
165+
width = istop - istart
166+
if step == 1 and width > 0:
157167
# Note that
158-
# int(istart + self.random()*(istop - istart))
168+
# int(istart + self.random()*width)
159169
# instead would be incorrect. For example, consider istart
160170
# = -2 and istop = 0. Then the guts would be in
161171
# -2.0 to 0.0 exclusive on both ends (ignoring that random()
162172
# might return 0.0), and because int() truncates toward 0, the
163173
# final result would be -1 or 0 (instead of -2 or -1).
164-
# istart + int(self.random()*(istop - istart))
174+
# istart + int(self.random()*width)
165175
# would also be incorrect, for a subtler reason: the RHS
166176
# can return a long, and then randrange() would also return
167177
# a long, but we're supposed to return an int (for backward
168178
# compatibility).
169-
return int(istart + int(self.random()*(istop - istart)))
179+
180+
if width >= maxwidth:
181+
return int(istart + self._randbelow(width))
182+
return int(istart + int(self.random()*width))
170183
if step == 1:
171-
raise ValueError, "empty range for randrange()"
184+
raise ValueError, "empty range for randrange() (%d,%d, %d)" % (istart, istop, width)
172185

173186
# Non-unit step argument supplied.
174187
istep = int(step)
175188
if istep != step:
176189
raise ValueError, "non-integer step for randrange()"
177190
if istep > 0:
178-
n = (istop - istart + istep - 1) / istep
191+
n = (width + istep - 1) / istep
179192
elif istep < 0:
180-
n = (istop - istart + istep + 1) / istep
193+
n = (width + istep + 1) / istep
181194
else:
182195
raise ValueError, "zero step for randrange()"
183196

184197
if n <= 0:
185198
raise ValueError, "empty range for randrange()"
199+
200+
if n >= maxwidth:
201+
return istart + self._randbelow(n)
186202
return istart + istep*int(self.random() * n)
187203

188204
def randint(self, a, b):
@@ -191,6 +207,33 @@ def randint(self, a, b):
191207

192208
return self.randrange(a, b+1)
193209

210+
def _randbelow(self, n, _log=_log, int=int, _maxwidth=1L<<BPF,
211+
_Method=_MethodType, _BuiltinMethod=_BuiltinMethodType):
212+
"""Return a random int in the range [0,n)
213+
214+
Handles the case where n has more bits than returned
215+
by a single call to the underlying generator.
216+
"""
217+
218+
try:
219+
getrandbits = self.getrandbits
220+
except AttributeError:
221+
pass
222+
else:
223+
# Only call self.getrandbits if the original random() builtin method
224+
# has not been overridden or if a new getrandbits() was supplied.
225+
# This assures that the two methods correspond.
226+
if type(self.random) is _BuiltinMethod or type(getrandbits) is _Method:
227+
k = int(1.00001 + _log(n-1, 2.0)) # 2**k > n-1 > 2**(k-2)
228+
r = getrandbits(k)
229+
while r >= n:
230+
r = getrandbits(k)
231+
return r
232+
if n >= _maxwidth:
233+
_warn("Underlying random() generator does not supply \n"
234+
"enough bits to choose from a population range this large")
235+
return int(self.random() * n)
236+
194237
## -------------------- sequence methods -------------------
195238

196239
def choice(self, seq):
@@ -757,6 +800,7 @@ def _test(N=2000):
757800
getstate = _inst.getstate
758801
setstate = _inst.setstate
759802
jumpahead = _inst.jumpahead
803+
getrandbits = _inst.getrandbits
760804

761805
if __name__ == '__main__':
762806
_test()

Lib/test/test_random.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import random
55
import time
66
import pickle
7+
import warnings
78
from math import log, exp, sqrt, pi
89
from sets import Set
910
from test import test_support
@@ -153,6 +154,13 @@ def test_gauss_with_whseed(self):
153154
self.assertEqual(x1, x2)
154155
self.assertEqual(y1, y2)
155156

157+
def test_bigrand(self):
158+
# Verify warnings are raised when randrange is too large for random()
159+
oldfilters = warnings.filters[:]
160+
warnings.filterwarnings("error", "Underlying random")
161+
self.assertRaises(UserWarning, self.gen.randrange, 2**60)
162+
warnings.filters[:] = oldfilters
163+
156164
class MersenneTwister_TestBasicOps(TestBasicOps):
157165
gen = random.Random()
158166

@@ -219,6 +227,76 @@ def test_long_seed(self):
219227
seed = (1L << (10000 * 8)) - 1 # about 10K bytes
220228
self.gen.seed(seed)
221229

230+
def test_53_bits_per_float(self):
231+
# This should pass whenever a C double has 53 bit precision.
232+
span = 2 ** 53
233+
cum = 0
234+
for i in xrange(100):
235+
cum |= int(self.gen.random() * span)
236+
self.assertEqual(cum, span-1)
237+
238+
def test_bigrand(self):
239+
# The randrange routine should build-up the required number of bits
240+
# in stages so that all bit positions are active.
241+
span = 2 ** 500
242+
cum = 0
243+
for i in xrange(100):
244+
r = self.gen.randrange(span)
245+
self.assert_(0 <= r < span)
246+
cum |= r
247+
self.assertEqual(cum, span-1)
248+
249+
def test_bigrand_ranges(self):
250+
for i in [40,80, 160, 200, 211, 250, 375, 512, 550]:
251+
start = self.gen.randrange(2 ** i)
252+
stop = self.gen.randrange(2 ** (i-2))
253+
if stop <= start:
254+
return
255+
self.assert_(start <= self.gen.randrange(start, stop) < stop)
256+
257+
def test_rangelimits(self):
258+
for start, stop in [(-2,0), (-(2**60)-2,-(2**60)), (2**60,2**60+2)]:
259+
self.assertEqual(Set(range(start,stop)),
260+
Set([self.gen.randrange(start,stop) for i in xrange(100)]))
261+
262+
def test_genrandbits(self):
263+
# Verify cross-platform repeatability
264+
self.gen.seed(1234567)
265+
self.assertEqual(self.gen.getrandbits(100),
266+
97904845777343510404718956115L)
267+
# Verify ranges
268+
for k in xrange(1, 1000):
269+
self.assert_(0 <= self.gen.getrandbits(k) < 2**k)
270+
271+
# Verify all bits active
272+
getbits = self.gen.getrandbits
273+
for span in [1, 2, 3, 4, 31, 32, 32, 52, 53, 54, 119, 127, 128, 129]:
274+
cum = 0
275+
for i in xrange(100):
276+
cum |= getbits(span)
277+
self.assertEqual(cum, 2**span-1)
278+
279+
def test_randbelow_logic(self, _log=log, int=int):
280+
# check bitcount transition points: 2**i and 2**(i+1)-1
281+
# show that: k = int(1.001 + _log(n, 2))
282+
# is equal to or one greater than the number of bits in n
283+
for i in xrange(1, 1000):
284+
n = 1L << i # check an exact power of two
285+
numbits = i+1
286+
k = int(1.00001 + _log(n, 2))
287+
self.assertEqual(k, numbits)
288+
self.assert_(n == 2**(k-1))
289+
290+
n += n - 1 # check 1 below the next power of two
291+
k = int(1.00001 + _log(n, 2))
292+
self.assert_(k in [numbits, numbits+1])
293+
self.assert_(2**k > n > 2**(k-2))
294+
295+
n -= n >> 15 # check a little farther below the next power of two
296+
k = int(1.00001 + _log(n, 2))
297+
self.assertEqual(k, numbits) # note the stronger assertion
298+
self.assert_(2**k > n > 2**(k-1)) # note the stronger assertion
299+
222300
_gammacoeff = (0.9999999999995183, 676.5203681218835, -1259.139216722289,
223301
771.3234287757674, -176.6150291498386, 12.50734324009056,
224302
-0.1385710331296526, 0.9934937113930748e-05, 0.1659470187408462e-06)

Misc/NEWS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ Library
8484
seed. Modified to match Py2.2 behavior and use fractional seconds so
8585
that successive runs are more likely to produce different sequences.
8686

87+
- random.Random has a new method, getrandbits(k), which returns an int
88+
with k random bits. This method is now an optional part of the API
89+
for user defined generators. Any generator that defines genrandbits()
90+
can now use randrange() for ranges with a length >= 2**53. Formerly,
91+
randrange would return only even numbers for ranges that large (see
92+
SF bug #812202). Generators that do not define genrandbits() now
93+
issue a warning when randrange() is called with a range that large.
94+
8795
- itertools.izip() with no arguments now returns an empty iterator instead
8896
of raising a TypeError exception.
8997

Modules/_randommodule.c

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,47 @@ random_jumpahead(RandomObject *self, PyObject *n)
434434
return Py_None;
435435
}
436436

437+
static PyObject *
438+
random_getrandbits(RandomObject *self, PyObject *args)
439+
{
440+
int k, i, bytes;
441+
unsigned long r;
442+
unsigned char *bytearray;
443+
PyObject *result;
444+
445+
if (!PyArg_ParseTuple(args, "i:getrandbits", &k))
446+
return NULL;
447+
448+
if (k <= 0) {
449+
PyErr_SetString(PyExc_ValueError,
450+
"number of bits must be greater than zero");
451+
return NULL;
452+
}
453+
454+
bytes = ((k - 1) / 32 + 1) * 4;
455+
bytearray = (unsigned char *)PyMem_Malloc(bytes);
456+
if (bytearray == NULL) {
457+
PyErr_NoMemory();
458+
return NULL;
459+
}
460+
461+
/* Fill-out whole words, byte-by-byte to avoid endianness issues */
462+
for (i=0 ; i<bytes ; i+=4, k-=32) {
463+
r = genrand_int32(self);
464+
if (k < 32)
465+
r >>= (32 - k);
466+
bytearray[i+0] = (unsigned char)r;
467+
bytearray[i+1] = (unsigned char)(r >> 8);
468+
bytearray[i+2] = (unsigned char)(r >> 16);
469+
bytearray[i+3] = (unsigned char)(r >> 24);
470+
}
471+
472+
/* little endian order to match bytearray assignment order */
473+
result = _PyLong_FromByteArray(bytearray, bytes, 1, 0);
474+
PyMem_Free(bytearray);
475+
return result;
476+
}
477+
437478
static PyObject *
438479
random_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
439480
{
@@ -464,6 +505,9 @@ static PyMethodDef random_methods[] = {
464505
{"jumpahead", (PyCFunction)random_jumpahead, METH_O,
465506
PyDoc_STR("jumpahead(int) -> None. Create new state from "
466507
"existing state and integer.")},
508+
{"getrandbits", (PyCFunction)random_getrandbits, METH_VARARGS,
509+
PyDoc_STR("getrandbits(k) -> x. Generates a long int with "
510+
"k random bits.")},
467511
{NULL, NULL} /* sentinel */
468512
};
469513

0 commit comments

Comments
 (0)